From 1454a75986d7ddf4fa4414de399ba29aac50fa6e Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 14 Nov 2024 15:25:12 +0000 Subject: [PATCH 01/16] [Ownership] Assign test files to search kibana team (#200013) ## Summary Assign test files to search kibana team Contributes to: #192979 --- .github/CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 745101711171c..ff5ec12fef6a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1646,6 +1646,11 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/test/functional/es_archives/cases/signals/hosts_users @elastic/response-ops # Enterprise Search +# search +/x-pack/test/functional_solution_sidenav/tests/search_sidenav.ts @elastic/search-kibana +/x-pack/test/functional/services/search_sessions.ts @elastic/search-kibana +/x-pack/test/functional/page_objects/search_* @elastic/search-kibana +/x-pack/test/functional/apps/search_playground @elastic/search-kibana /x-pack/test_serverless/functional/page_objects/svl_ingest_pipelines.ts @elastic/search-kibana /x-pack/test/functional/apps/dev_tools/embedded_console.ts @elastic/search-kibana /x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @elastic/search-kibana From 04b2d62fee8731e1a0a6ff230b512d7e94843ce5 Mon Sep 17 00:00:00 2001 From: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:39:11 +0100 Subject: [PATCH 02/16] Migrate /diagnostics and /service_nodes to be deployment agnostic (#199645) closes #198967 closes #198985 part of https://github.com/elastic/kibana/issues/193245 ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) ### Checklist - [x] (OPTIONAL, only if a test has been unskipped) Run flaky test suite - [x] serverless - [x] stateful - [x] MKI --------- Co-authored-by: Sergi Romeu --- .../observability/apm/agent_explorer/index.ts | 2 +- .../apm/alerts/error_count_threshold.spec.ts | 2 +- .../apis/observability/apm/alerts/index.ts | 2 +- .../alerts/preview_chart_error_count.spec.ts | 2 +- .../alerts/preview_chart_error_rate.spec.ts | 2 +- .../observability/apm/cold_start/index.ts | 2 +- .../observability/apm/correlations/index.ts | 2 +- .../apm/custom_dashboards/index.ts | 2 +- .../apis/observability/apm/data_view/index.ts | 2 +- .../observability/apm/dependencies/index.ts | 2 +- .../apm/dependencies/top_dependencies.spec.ts | 2 +- .../apm}/diagnostics/apm_events.spec.ts | 16 +-- .../apm}/diagnostics/data_streams.spec.ts | 19 ++- .../observability/apm/diagnostics/index.ts | 19 +++ .../index_pattern_settings.spec.ts | 108 ++++++++++++++++++ .../apm}/diagnostics/index_templates.spec.ts | 40 ++----- .../apm}/diagnostics/indices.spec.ts | 21 ++-- .../apm}/diagnostics/privileges.spec.ts | 9 +- .../apis/observability/apm/entities/index.ts | 2 +- .../observability/apm/environment/index.ts | 2 +- .../observability/apm/error_rate/index.ts | 2 +- .../apm/error_rate/service_apis.spec.ts | 2 +- .../apm/error_rate/service_maps.spec.ts | 4 +- .../apm/historical_data/has_data.spec.ts | 4 +- .../apm/historical_data/index.ts | 2 +- .../apis/observability/apm/index.ts | 4 +- .../observability/apm/infrastructure/index.ts | 2 +- .../infrastructure_attributes.spec.ts | 2 +- .../apis/observability/apm/latency/index.ts | 2 +- .../apm/latency/service_apis.spec.ts | 4 +- .../apm/latency/service_maps.spec.ts | 4 +- .../apm/mobile/generate_mobile_data.ts | 2 +- .../apis/observability/apm/mobile/index.ts | 2 +- .../apm/observability_overview/index.ts | 2 +- .../observability_overview.spec.ts | 2 +- .../observability/apm/service_groups/index.ts | 2 +- .../service_groups/save_service_group.spec.ts | 2 +- .../service_group_count.spec.ts | 4 +- .../service_group_with_overflow.spec.ts | 4 +- .../service_nodes/get_service_nodes.spec.ts | 100 ++++++++++++++++ .../observability/apm/service_nodes/index.ts | 14 +++ .../diagnostics/index_pattern_settings.ts | 106 ----------------- .../service_nodes/get_service_nodes.spec.ts | 96 ---------------- 43 files changed, 321 insertions(+), 305 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/diagnostics/apm_events.spec.ts (93%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/diagnostics/data_streams.spec.ts (82%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_pattern_settings.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/diagnostics/index_templates.spec.ts (51%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/diagnostics/indices.spec.ts (88%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/diagnostics/privileges.spec.ts (90%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/get_service_nodes.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/index.ts delete mode 100644 x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts delete mode 100644 x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts index f77b13923930a..ae05cb6b89cbe 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/agent_explorer/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('agent_explorer', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts index e0b9e1b022b4f..c515263f09b2e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/error_count_threshold.spec.ts @@ -10,7 +10,7 @@ import { errorCountActionVariables } from '@kbn/apm-plugin/server/routes/alerts/ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts index 71661e4cbc8bc..3796b4253e60e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('alerts', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts index d6792400fc2bc..38bf9d4eade4d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_count.spec.ts @@ -7,7 +7,7 @@ import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; import expect from '@kbn/expect'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { ERROR_GROUP_ID, SERVICE_ENVIRONMENT, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts index 3e5c0753fbc1d..167b114e755c3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/preview_chart_error_rate.spec.ts @@ -13,7 +13,7 @@ import { } from '@kbn/observability-shared-plugin/common'; import type { PreviewChartResponseItem } from '@kbn/apm-plugin/server/routes/alerts/route'; import expect from '@kbn/expect'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateErrorData } from './generate_data'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts index a5d8045f227d3..108340514a0c2 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/cold_start/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('cold_start', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts index 660556edb8d79..ffa54dbe7bb5c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('correlations', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts index 77f12ca0f88d5..a46024ff1ab10 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/custom_dashboards/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('custom_dashboards', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts index 9412ddca7cbcb..c81b643e62638 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/data_view/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('data_view', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts index 2acf449ce923d..46ad399380550 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('custom_dashboards', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts index 0fa88b67d3379..21e990a1dbb52 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/dependencies/top_dependencies.spec.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { NodeType, DependencyNode } from '@kbn/apm-plugin/common/connections'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { dataConfig, generateData } from './generate_data'; import { roundNumber } from '../utils/common'; diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/apm_events.spec.ts similarity index 93% rename from x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/apm_events.spec.ts index 1abace6c77f4c..4ca60e15fa190 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/apm_events.spec.ts @@ -9,19 +9,18 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { sumBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - // FLAKY: https://github.com/elastic/kibana/issues/177144 - registry.when('Diagnostics: APM Events', { config: 'basic', archives: [] }, () => { + describe('Diagnostics: APM Events', () => { describe('When there is no data', () => { before(async () => { // delete APM data streams @@ -38,10 +37,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index( timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/data_streams.spec.ts similarity index 82% rename from x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/data_streams.spec.ts index 80fa34dbaa002..1d37c668e91f5 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/data_streams.spec.ts @@ -7,20 +7,18 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - // FLAKY: https://github.com/elastic/kibana/issues/177245 - registry.when('Diagnostics: Data streams', { config: 'basic', archives: [] }, () => { + describe('Diagnostics: Data streams', () => { describe('When there is no data', () => { before(async () => { // delete APM data streams @@ -45,9 +43,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index.ts new file mode 100644 index 0000000000000..192771d3fc75e --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index.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 type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('diagnostics', () => { + loadTestFile(require.resolve('./apm_events.spec.ts')); + loadTestFile(require.resolve('./data_streams.spec.ts')); + loadTestFile(require.resolve('./index_pattern_settings.spec.ts')); + loadTestFile(require.resolve('./index_templates.spec.ts')); + loadTestFile(require.resolve('./indices.spec.ts')); + loadTestFile(require.resolve('./privileges.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_pattern_settings.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_pattern_settings.spec.ts new file mode 100644 index 0000000000000..8235e2a179d4c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_pattern_settings.spec.ts @@ -0,0 +1,108 @@ +/* + * 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 expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { uniq } from 'lodash'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + describe('Diagnostics: Index pattern settings', () => { + describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const instance = apm + .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) + .instance('instance-a'); + await apmSynthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(30) + .generator((timestamp) => + instance + .transaction({ transactionName: 'GET /users' }) + .timestamp(timestamp) + .duration(100) + .success() + ) + ); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns APM index templates', async () => { + const apmIndexTemplatesPatterns = ['apm', 'otel']; + + const { status, body } = await apmApiClient.adminUser({ + endpoint: 'GET /internal/apm/diagnostics', + }); + expect(status).to.be(200); + + // filtering the array for unique index templates because they get duplicated across different index patterns + const uniqueTemplateNames = uniq( + body.indexTemplatesByIndexPattern.flatMap(({ indexTemplates }) => { + return indexTemplates?.map(({ templateName }) => templateName); + }) + ); + + // filter only APM releated indices + const apmTemplateNames = uniqueTemplateNames.filter( + (templateName) => + templateName.endsWith('@template') && + apmIndexTemplatesPatterns.some((pattern) => templateName.includes(pattern)) + ); + + // sort alphabeticaly before comparing because an order is different between testing environments + const sortedApmTemplates = apmTemplateNames.sort(); + + expect(sortedApmTemplates).to.eql([ + 'logs-apm.app@template', + 'logs-apm.error@template', + 'logs-otel@template', + 'metrics-apm.app@template', + 'metrics-apm.internal@template', + 'metrics-apm.service_destination.10m@template', + 'metrics-apm.service_destination.1m@template', + 'metrics-apm.service_destination.60m@template', + 'metrics-apm.service_summary.10m@template', + 'metrics-apm.service_summary.1m@template', + 'metrics-apm.service_summary.60m@template', + 'metrics-apm.service_transaction.10m@template', + 'metrics-apm.service_transaction.1m@template', + 'metrics-apm.service_transaction.60m@template', + 'metrics-apm.transaction.10m@template', + 'metrics-apm.transaction.1m@template', + 'metrics-apm.transaction.60m@template', + 'metrics-otel@template', + 'metrics-service_summary.10m.otel@template', + 'metrics-service_summary.1m.otel@template', + 'metrics-service_summary.60m.otel@template', + 'metrics-service_transaction.10m.otel@template', + 'metrics-service_transaction.1m.otel@template', + 'metrics-service_transaction.60m.otel@template', + 'metrics-transaction.10m.otel@template', + 'metrics-transaction.1m.otel@template', + 'metrics-transaction.60m.otel@template', + 'traces-apm.rum@template', + 'traces-apm.sampled@template', + 'traces-apm@template', + 'traces-otel@template', + ]); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_templates.spec.ts similarity index 51% rename from x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_templates.spec.ts index 1bbc799b3bf78..aa45a93a3ce73 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/index_templates.spec.ts @@ -7,44 +7,22 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { getApmIndexTemplateNames } from '@kbn/apm-plugin/server/routes/diagnostics/helpers/get_apm_index_template_names'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - registry.when.skip('Diagnostics: Index Templates', { config: 'basic', archives: [] }, () => { - describe('When there is no data', () => { - before(async () => { - // delete APM index templates - await es.indices.deleteIndexTemplate({ - name: Object.values(getApmIndexTemplateNames()).flat(), - }); - }); - - it('verifies that none of the default APM index templates exists`', async () => { - const { status, body } = await apmApiClient.adminUser({ - endpoint: 'GET /internal/apm/diagnostics', - }); - expect(status).to.be(200); - const noApmIndexTemplateExists = body.apmIndexTemplates.every( - ({ exists }) => exists === false - ); - expect(noApmIndexTemplateExists).to.eql(true); - }); - }); - + describe('Diagnostics: Index Templates', () => { describe('When data is ingested', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/indices.spec.ts similarity index 88% rename from x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/indices.spec.ts index 477824524b48c..92976e6bce883 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/indices.spec.ts @@ -8,20 +8,20 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const es = getService('es'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - // FLAKY: https://github.com/elastic/kibana/pull/177039 - registry.when.skip('Diagnostics: Indices', { config: 'basic', archives: [] }, () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + describe('Diagnostics: Indices', () => { describe.skip('When there is no data', () => { it('returns empty response`', async () => { const { status, body } = await apmApiClient.adminUser({ @@ -39,6 +39,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index( timerange(start, end) @@ -92,8 +93,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await es.indices.delete({ index: 'traces-apm-default' }); - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); await apmSynthtraceEsClient.clean(); }); @@ -136,8 +135,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); after(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); await apmSynthtraceEsClient.clean(); }); diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/privileges.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/privileges.spec.ts similarity index 90% rename from x-pack/test/apm_api_integration/tests/diagnostics/privileges.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/privileges.spec.ts index 2d9652b612010..fa46ff08cc8ed 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/privileges.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/diagnostics/privileges.spec.ts @@ -7,13 +7,12 @@ import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); - registry.when('Diagnostics: Privileges', { config: 'basic', archives: [] }, () => { + describe('Diagnostics: Privileges', () => { describe('superuser', () => { let body: APIReturnType<'GET /internal/apm/diagnostics'>; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts index d7a36e3e447b7..71be4955fe523 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('entities', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts index 4a77e610d5000..e48a8f88d74c4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/environment/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('environment', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts index a3dd89f0ddb1a..04ed9b3be1ff0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('error_rate', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts index 56dded824a32d..6a8bca2eefaca 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_apis.spec.ts @@ -13,7 +13,7 @@ import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; const GO_PROD_LIST_RATE = 75; const GO_PROD_LIST_ERROR_RATE = 25; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts index 462ad8db4bdda..c45f3fd9457d8 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/error_rate/service_maps.spec.ts @@ -5,13 +5,13 @@ * 2.0. */ import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import expect from '@kbn/expect'; import { meanBy } from 'lodash'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; const GO_PROD_LIST_RATE = 75; const GO_PROD_LIST_ERROR_RATE = 25; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts index 6ac96b8e38154..b6eaca65cad1c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/has_data.spec.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import moment from 'moment'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts index 49f0068ee313b..0b9c76d21d3b4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/historical_data/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('historical_data', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index af8cc32783c20..d115e10f2373a 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile, @@ -30,5 +30,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./infrastructure')); loadTestFile(require.resolve('./inspect')); loadTestFile(require.resolve('./service_groups')); + loadTestFile(require.resolve('./diagnostics')); + loadTestFile(require.resolve('./service_nodes')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts index 1351a44d41c8e..3379e3f0fdc23 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('infrastructure', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts index ca70d8d1fa002..fe9b0246c505b 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/infrastructure/infrastructure_attributes.spec.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { generateData } from './generate_data'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts index 0b9a71293f687..2ff9ec1896c78 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('latency', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts index dee5f27b7a61d..2219a873c0f2d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_apis.spec.ts @@ -12,8 +12,8 @@ import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts index fa088e4f12dc9..e0bb69306a03f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/latency/service_maps.spec.ts @@ -10,8 +10,8 @@ import { meanBy } from 'lodash'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { const apmApiClient = getService('apmApi'); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts index a4420b3f53c7c..fa3323c411d61 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/generate_mobile_data.ts @@ -5,7 +5,7 @@ * 2.0. */ import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const SERVICE_VERSIONS = ['2.3', '1.2', '1.1']; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts index 97d8e13256d60..d54eb187b29c6 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/mobile/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('Mobile', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts index c43e15d005bb9..bbb35976f8175 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('observability_overview', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts index 740dd432b670b..796ef08f70250 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/observability_overview/observability_overview.spec.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { roundNumber } from '../../../../../../apm_api_integration/utils'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts index e88208d48a9b5..458361077c208 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('service_groups', () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts index a0ed02739cf9f..0b4db96e880e3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/save_service_group.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { ApmApiError } from '../../../../../../apm_api_integration/common/apm_api_supertest'; import { expectToReject } from '../../../../../../apm_api_integration/common/utils/expect_to_reject'; import { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts index 21ac03197a422..cbb29e2729dcb 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_count/service_group_count.spec.ts @@ -5,8 +5,8 @@ * 2.0. */ import expect from '@kbn/expect'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { createServiceGroupApi, deleteAllServiceGroups, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts index 9324dee60d4e7..b5cbf1ae2566c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts @@ -9,8 +9,8 @@ import { ValuesType } from 'utility-types'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { createServiceGroupApi, deleteAllServiceGroups } from '../service_groups_api_methods'; import { createServiceTransactionMetricsDocs } from './es_utils'; import { generateData } from './generate_data'; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/get_service_nodes.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/get_service_nodes.spec.ts new file mode 100644 index 0000000000000..95f0b8defa989 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/get_service_nodes.spec.ts @@ -0,0 +1,100 @@ +/* + * 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 expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const serviceName = 'synth-go'; + const instanceName = 'instance-a'; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/nodes', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + kuery: '', + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + } + + describe('Service nodes', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceNodes": Array [], + } + `); + }); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + const instance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance(instanceName); + await apmSynthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + instance + .appMetrics({ + 'system.process.cpu.total.norm.pct': 1, + 'jvm.memory.heap.used': 1000, + 'jvm.memory.non_heap.used': 100, + 'jvm.thread.count': 25, + }) + .timestamp(timestamp) + ) + ); + }); + after(() => apmSynthtraceEsClient.clean()); + + it('returns service nodes', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceNodes": Array [ + Object { + "cpu": 1, + "heapMemory": 1000, + "hostName": "instance-a", + "name": "instance-a", + "nonHeapMemory": 100, + "threadCount": 25, + }, + ], + } + `); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/index.ts new file mode 100644 index 0000000000000..18315aade3b31 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_nodes/index.ts @@ -0,0 +1,14 @@ +/* + * 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 type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('service_nodes', () => { + loadTestFile(require.resolve('./get_service_nodes.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts b/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts deleted file mode 100644 index d0ba7b1850d31..0000000000000 --- a/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts +++ /dev/null @@ -1,106 +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 expect from '@kbn/expect'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { getApmIndexTemplateNames } from '@kbn/apm-plugin/server/routes/diagnostics/helpers/get_apm_index_template_names'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const es = getService('es'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const synthtraceKibanaClient = getService('synthtraceKibanaClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - - registry.when('Diagnostics: Index pattern settings', { config: 'basic', archives: [] }, () => { - describe('When there is no data', () => { - before(async () => { - // delete APM index templates - await es.indices.deleteIndexTemplate({ - name: Object.values(getApmIndexTemplateNames()).flat(), - }); - }); - - it('returns the built-in (non-APM) index templates`', async () => { - const { status, body } = await apmApiClient.adminUser({ - endpoint: 'GET /internal/apm/diagnostics', - }); - expect(status).to.be(200); - - const templateNames = body.indexTemplatesByIndexPattern.flatMap(({ indexTemplates }) => { - return indexTemplates?.map(({ templateName }) => templateName); - }); - - expect(templateNames).to.eql(['logs', 'metrics']); - }); - }); - - describe('When data is ingested', () => { - before(async () => { - const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); - await synthtraceKibanaClient.installApmPackage(latestVersion); - - const instance = apm - .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) - .instance('instance-a'); - await apmSynthtraceEsClient.index( - timerange(start, end) - .interval('1m') - .rate(30) - .generator((timestamp) => - instance - .transaction({ transactionName: 'GET /users' }) - .timestamp(timestamp) - .duration(100) - .success() - ) - ); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns APM index templates', async () => { - const { status, body } = await apmApiClient.adminUser({ - endpoint: 'GET /internal/apm/diagnostics', - }); - expect(status).to.be(200); - - const templateNames = body.indexTemplatesByIndexPattern.flatMap(({ indexTemplates }) => { - return indexTemplates?.map(({ templateName }) => templateName); - }); - - expect(templateNames).to.eql([ - 'logs-apm.error', - 'logs-apm.app', - 'logs', - 'metrics-apm.service_transaction.60m', - 'metrics-apm.service_destination.10m', - 'metrics-apm.transaction.1m', - 'metrics-apm.service_destination.1m', - 'metrics-apm.service_transaction.10m', - 'metrics-apm.service_transaction.1m', - 'metrics-apm.transaction.60m', - 'metrics-apm.service_destination.60m', - 'metrics-apm.service_summary.1m', - 'metrics-apm.transaction.10m', - 'metrics-apm.internal', - 'metrics-apm.service_summary.10m', - 'metrics-apm.service_summary.60m', - 'metrics-apm.app', - 'metrics', - 'traces-apm', - 'traces-apm.rum', - 'traces-apm.sampled', - ]); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts deleted file mode 100644 index 6b24587b6bc13..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts +++ /dev/null @@ -1,96 +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 expect from '@kbn/expect'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - const serviceName = 'synth-go'; - const instanceName = 'instance-a'; - - async function callApi() { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/nodes', - params: { - path: { serviceName }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - kuery: '', - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - } - - registry.when('Service nodes when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callApi(); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "serviceNodes": Array [], - } - `); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177496 - registry.when('Service nodes when data is loaded', { config: 'basic', archives: [] }, () => { - before(async () => { - const instance = apm - .service({ name: serviceName, environment: 'production', agentName: 'go' }) - .instance(instanceName); - await apmSynthtraceEsClient.index( - timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => - instance - .appMetrics({ - 'system.process.cpu.total.norm.pct': 1, - 'jvm.memory.heap.used': 1000, - 'jvm.memory.non_heap.used': 100, - 'jvm.thread.count': 25, - }) - .timestamp(timestamp) - ) - ); - }); - after(() => apmSynthtraceEsClient.clean()); - - it('returns service nodes', async () => { - const response = await callApi(); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "serviceNodes": Array [ - Object { - "cpu": 1, - "heapMemory": 1000, - "hostName": "instance-a", - "name": "instance-a", - "nonHeapMemory": 100, - "threadCount": 25, - }, - ], - } - `); - }); - }); -} From f3e773552d2f98d26afd8e9df866c812a5e6fdd7 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 14 Nov 2024 08:43:56 -0700 Subject: [PATCH 03/16] [presentation-utils] remove defaultTheme$ (#200044) Closes https://github.com/elastic/kibana/issues/200037 defaultTheme$ is only used in storybooks. This PR removes defaultTheme$ and replaces it with theme$ available from core start mock. --- .../__stories__/error_renderer.stories.tsx | 6 +++++- .../expression_renderers/debug_renderer.tsx | 4 +--- .../expression_renderers/error_renderer.tsx | 4 +--- .../__stories__/image_renderer.stories.tsx | 10 +++++++++- .../expression_renderers/image_renderer.tsx | 5 ++--- .../__stories__/metric_renderer.stories.tsx | 17 ++++++++++------- .../expression_renderers/metric_renderer.tsx | 4 +--- .../repeat_image_renderer.stories.tsx | 9 ++++++++- .../repeat_image_renderer.tsx | 5 ++--- .../reveal_image_renderer.stories.tsx | 9 ++++++++- .../reveal_image_renderer.tsx | 4 +--- .../__stories__/progress_renderer.stories.tsx | 5 ++++- .../__stories__/shape_renderer.stories.tsx | 5 ++++- .../expression_renderers/progress_renderer.tsx | 4 +--- .../expression_renderers/shape_renderer.tsx | 4 +--- src/plugins/presentation_util/common/index.ts | 1 - .../common/lib/utils/default_theme.ts | 15 --------------- .../presentation_util/common/lib/utils/index.ts | 1 - 18 files changed, 58 insertions(+), 54 deletions(-) delete mode 100644 src/plugins/presentation_util/common/lib/utils/default_theme.ts diff --git a/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx b/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx index 07451f1f2c325..b7f789f873de3 100644 --- a/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx +++ b/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getErrorRenderer } from '../error_renderer'; @@ -17,5 +18,8 @@ storiesOf('renderers/error', module).add('default', () => { const config = { error: thrownError, }; - return ; + + return ( + + ); }); diff --git a/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx b/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx index c3d4220038870..29fa69aa736b4 100644 --- a/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx +++ b/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx @@ -17,7 +17,6 @@ import { i18n } from '@kbn/i18n'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { JSON } from '../../common'; import { LazyDebugRenderComponent } from '../components'; @@ -38,8 +37,7 @@ const strings = { }; export const getDebugRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'debug', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx b/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx index c7865525b21f6..4ba3daa15d08c 100644 --- a/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx +++ b/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx @@ -21,7 +21,6 @@ import { import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { ErrorRendererConfig } from '../../common/types'; import { LazyErrorRenderComponent } from '../components'; @@ -39,8 +38,7 @@ const errorStrings = { const ErrorComponent = withSuspense(LazyErrorRenderComponent); export const getErrorRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'error', displayName: errorStrings.getDisplayName(), help: errorStrings.getHelpDescription(), diff --git a/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx b/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx index 02679899b112d..07fb4db558bd5 100644 --- a/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx +++ b/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render, waitFor } from '@kbn/presentation-util-plugin/public/__stories__'; import { getElasticLogo } from '@kbn/presentation-util-plugin/common'; import { getImageRenderer } from '../image_renderer'; @@ -20,7 +21,14 @@ const Renderer = ({ elasticLogo }: { elasticLogo: string }) => { mode: ImageMode.COVER, }; - return ; + return ( + + ); }; storiesOf('renderers/image', module).add( diff --git a/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx b/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx index 8954bd90dc974..e80281fc10f42 100644 --- a/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx +++ b/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx @@ -17,7 +17,7 @@ import { IInterpreterRenderHandlers, } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; -import { getElasticLogo, defaultTheme$, isValidUrl } from '@kbn/presentation-util-plugin/common'; +import { getElasticLogo, isValidUrl } from '@kbn/presentation-util-plugin/common'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; import { ImageRendererConfig } from '../../common/types'; @@ -34,8 +34,7 @@ const strings = { }; export const getImageRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'image', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx b/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx index 03c8f9d5048d5..5438bb4b4287a 100644 --- a/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx +++ b/src/plugins/expression_metric/public/expression_renderers/__stories__/metric_renderer.stories.tsx @@ -9,6 +9,7 @@ import React, { CSSProperties } from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Style } from '@kbn/expressions-plugin/common'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getMetricRenderer } from '../metric_renderer'; @@ -37,6 +38,8 @@ const metricFontSpec: CSSProperties = { color: '#b83c6f', }; +const theme$ = coreMock.createStart().theme.theme$; + storiesOf('renderers/Metric', module) .add('with null metric', () => { const config: MetricRendererConfig = { @@ -46,7 +49,7 @@ storiesOf('renderers/Metric', module) label: '', metricFormat: '', }; - return ; + return ; }) .add('with number metric', () => { const config: MetricRendererConfig = { @@ -56,7 +59,7 @@ storiesOf('renderers/Metric', module) label: '', metricFormat: '', }; - return ; + return ; }) .add('with string metric', () => { const config: MetricRendererConfig = { @@ -66,7 +69,7 @@ storiesOf('renderers/Metric', module) label: '', metricFormat: '', }; - return ; + return ; }) .add('with label', () => { const config: MetricRendererConfig = { @@ -76,7 +79,7 @@ storiesOf('renderers/Metric', module) label: 'Average price', metricFormat: '', }; - return ; + return ; }) .add('with number metric and a specified format', () => { const config: MetricRendererConfig = { @@ -86,7 +89,7 @@ storiesOf('renderers/Metric', module) label: 'Average price', metricFormat: '0.00%', }; - return ; + return ; }) .add('with formatted string metric and a specified format', () => { const config: MetricRendererConfig = { @@ -96,7 +99,7 @@ storiesOf('renderers/Metric', module) label: 'Total Revenue', metricFormat: '$0a', }; - return ; + return ; }) .add('with invalid metricFont', () => { const config: MetricRendererConfig = { @@ -106,5 +109,5 @@ storiesOf('renderers/Metric', module) label: 'Total Revenue', metricFormat: '$0a', }; - return ; + return ; }); diff --git a/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx b/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx index b802f4f40dafb..db4b8575d5e26 100644 --- a/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx +++ b/src/plugins/expression_metric/public/expression_renderers/metric_renderer.tsx @@ -19,7 +19,6 @@ import { import { i18n } from '@kbn/i18n'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { MetricRendererConfig } from '../../common/types'; const strings = { @@ -34,8 +33,7 @@ const strings = { }; export const getMetricRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'metric', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx b/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx index fa666d98ebb9a..a5f58b97970cf 100644 --- a/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx +++ b/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getElasticLogo, getElasticOutline } from '@kbn/presentation-util-plugin/common'; import { waitFor } from '@kbn/presentation-util-plugin/public/__stories__'; @@ -29,7 +30,13 @@ const Renderer = ({ emptyImage: elasticOutline, }; - return ; + return ( + + ); }; storiesOf('enderers/repeatImage', module).add( diff --git a/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx b/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx index 6dcc132937060..9a35459880889 100644 --- a/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx +++ b/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$, getElasticOutline, isValidUrl } from '@kbn/presentation-util-plugin/common'; +import { getElasticOutline, isValidUrl } from '@kbn/presentation-util-plugin/common'; import { RepeatImageRendererConfig } from '../../common/types'; const strings = { @@ -35,8 +35,7 @@ const strings = { }; export const getRepeatImageRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'repeatImage', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx index 0c1a2cb5ca169..664cc97117791 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { getElasticOutline, getElasticLogo } from '@kbn/presentation-util-plugin/common'; import { Render, waitFor } from '@kbn/presentation-util-plugin/public/__stories__'; import { getRevealImageRenderer } from '..'; @@ -27,7 +28,13 @@ const Renderer = ({ origin: Origin.LEFT, percent: 0.45, }; - return ; + + return ( + + ); }; storiesOf('renderers/revealImage', module).add( diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx index 5beb58ef60f39..fbfe479225ece 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { RevealImageRendererConfig } from '../../common/types'; export const strings = { @@ -35,8 +34,7 @@ export const strings = { }; export const getRevealImageRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'revealImage', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx b/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx index 22e8864d1b7de..0f93314ee0816 100644 --- a/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; +import { coreMock } from '@kbn/core/public/mocks'; import { getProgressRenderer } from '../progress_renderer'; import { Progress } from '../../../common'; @@ -30,5 +31,7 @@ storiesOf('renderers/progress', module).add('default', () => { valueWeight: 15, }; - return ; + return ( + + ); }); diff --git a/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx b/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx index 77b54ae87239b..d089174c60325 100644 --- a/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { coreMock } from '@kbn/core/public/mocks'; import { Render } from '@kbn/presentation-util-plugin/public/__stories__'; import { getShapeRenderer } from '..'; import { Shape } from '../../../common/types'; @@ -23,5 +24,7 @@ storiesOf('renderers/shape', module).add('default', () => { maintainAspect: true, }; - return ; + return ( + + ); }); diff --git a/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx b/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx index 80904cf12f2b6..ed7629a7d87a0 100644 --- a/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { ProgressRendererConfig } from '../../common/types'; const strings = { @@ -35,8 +34,7 @@ const strings = { }; export const getProgressRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'progress', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx b/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx index 06c442ad2d8cd..650033aa4542d 100644 --- a/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; -import { defaultTheme$ } from '@kbn/presentation-util-plugin/common'; import { ShapeRendererConfig } from '../../common/types'; const strings = { @@ -35,8 +34,7 @@ const strings = { }; export const getShapeRenderer = - (theme$: Observable = defaultTheme$) => - (): ExpressionRenderDefinition => ({ + (theme$: Observable) => (): ExpressionRenderDefinition => ({ name: 'shape', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), diff --git a/src/plugins/presentation_util/common/index.ts b/src/plugins/presentation_util/common/index.ts index 6917c52f4edc9..b0350ca4a20f4 100644 --- a/src/plugins/presentation_util/common/index.ts +++ b/src/plugins/presentation_util/common/index.ts @@ -36,7 +36,6 @@ export { } from './labs'; export { - defaultTheme$, getElasticLogo, getElasticOutline, isValidUrl, diff --git a/src/plugins/presentation_util/common/lib/utils/default_theme.ts b/src/plugins/presentation_util/common/lib/utils/default_theme.ts deleted file mode 100644 index 9c86456bc2513..0000000000000 --- a/src/plugins/presentation_util/common/lib/utils/default_theme.ts +++ /dev/null @@ -1,15 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { CoreTheme } from '@kbn/core/public'; -import { Observable } from 'rxjs'; - -export const defaultTheme$: Observable = new Observable((subscriber) => - subscriber.next({ darkMode: false }) -); diff --git a/src/plugins/presentation_util/common/lib/utils/index.ts b/src/plugins/presentation_util/common/lib/utils/index.ts index 20b1d8450e5a5..091aa216aa835 100644 --- a/src/plugins/presentation_util/common/lib/utils/index.ts +++ b/src/plugins/presentation_util/common/lib/utils/index.ts @@ -11,7 +11,6 @@ export * from './dataurl'; export * from './httpurl'; export * from './resolve_dataurl'; export * from './url'; -export { defaultTheme$ } from './default_theme'; export async function getElasticLogo() { return await import('./elastic_logo'); From 50f0016cd7b01eabc280aca4131f843ff305231d Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 14 Nov 2024 07:54:51 -0800 Subject: [PATCH 04/16] [OpenAPI][DOCS] Add descriptions for alerting rule flapping properties (#200112) --- oas_docs/bundle.json | 18 ++++++ oas_docs/bundle.serverless.json | 18 ++++++ oas_docs/output/kibana.serverless.yaml | 60 +++++++++++++++++++ oas_docs/output/kibana.yaml | 60 +++++++++++++++++++ .../routes/rule/common/flapping/schemas/v1.ts | 13 +++- 5 files changed, 168 insertions(+), 1 deletion(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 0fde92c1fa3f9..a4c1e30c8cf05 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -1304,14 +1304,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2083,14 +2086,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2484,14 +2490,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3253,14 +3262,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3647,14 +3659,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -5094,14 +5109,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 229bda84b8629..a0bd0c4cc4340 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -1304,14 +1304,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2083,14 +2086,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -2484,14 +2490,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3253,14 +3262,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -3647,14 +3659,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" @@ -5094,14 +5109,17 @@ }, "flapping": { "additionalProperties": false, + "description": "When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.", "nullable": true, "properties": { "look_back_window": { + "description": "The minimum number of runs in which the threshold must be met.", "maximum": 20, "minimum": 2, "type": "number" }, "status_change_threshold": { + "description": "The minimum number of times an alert must switch states in the look back window.", "maximum": 20, "minimum": 2, "type": "number" diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 9f0c38baded7d..117e52586c5ad 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -1018,14 +1018,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number @@ -1600,14 +1610,24 @@ paths: type: boolean flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch states + in the look back window. maximum: 20 minimum: 2 type: number @@ -1925,14 +1945,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number @@ -2510,14 +2540,24 @@ paths: - active flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch states + in the look back window. maximum: 20 minimum: 2 type: number @@ -2807,14 +2847,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number @@ -3852,14 +3902,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 50fd92fdc8a9c..82acbdb311f2f 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -1367,14 +1367,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number @@ -1948,14 +1958,24 @@ paths: type: boolean flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch states + in the look back window. maximum: 20 minimum: 2 type: number @@ -2273,14 +2293,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number @@ -2857,14 +2887,24 @@ paths: - active flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch states + in the look back window. maximum: 20 minimum: 2 type: number @@ -3154,14 +3194,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number @@ -4191,14 +4241,24 @@ paths: - last_execution_date flapping: additionalProperties: false + description: >- + When flapping detection is turned on, alerts that switch + quickly between active and recovered states are identified + as “flapping” and notifications are reduced. nullable: true type: object properties: look_back_window: + description: >- + The minimum number of runs in which the threshold must + be met. maximum: 20 minimum: 2 type: number status_change_threshold: + description: >- + The minimum number of times an alert must switch + states in the look back window. maximum: 20 minimum: 2 type: number diff --git a/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts index 4844a9e808ef5..acdd35854df3f 100644 --- a/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/common/flapping/schemas/v1.ts @@ -17,13 +17,24 @@ import { validateFlapping as validateFlappingV1 } from '../../../validation/vali export const flappingSchema = schema.object( { look_back_window: schema.number({ + meta: { description: 'The minimum number of runs in which the threshold must be met.' }, min: MIN_LOOK_BACK_WINDOW_V1, max: MAX_LOOK_BACK_WINDOW_V1, }), status_change_threshold: schema.number({ + meta: { + description: + 'The minimum number of times an alert must switch states in the look back window.', + }, min: MIN_STATUS_CHANGE_THRESHOLD_V1, max: MAX_STATUS_CHANGE_THRESHOLD_V1, }), }, - { validate: validateFlappingV1 } + { + validate: validateFlappingV1, + meta: { + description: + 'When flapping detection is turned on, alerts that switch quickly between active and recovered states are identified as “flapping” and notifications are reduced.', + }, + } ); From 53c05a33e7fe917b7d64b7d3ca664aaf3f9cf3ca Mon Sep 17 00:00:00 2001 From: Viduni Wickramarachchi Date: Thu, 14 Nov 2024 11:09:42 -0500 Subject: [PATCH 05/16] [Obs AI Assistant] Add retry statements as an attempt to resolve flaky tests (#200022) Closes https://github.com/elastic/kibana/issues/192222 ## Summary ### Problem The test appears to be flaky, potentially because the entries are not available at the time of retrieval. This cannot be reproduced locally or via the flaky test runner. (more details [here](https://github.com/elastic/kibana/pull/196026#issuecomment-2409056856)) ### Solution Add a retry when fetching the instructions and check whether the number of instructions returned by the API endpoint is the same number of instructions expected. ### Checklist - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --- .../knowledge_base_user_instructions.spec.ts | 104 ++++++++++-------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts index 04791909340ef..6ea2b279fd386 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -30,6 +30,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const es = getService('es'); const ml = getService('ml'); const log = getService('log'); + const retry = getService('retry'); const getScopedApiClientForUsername = getService('getScopedApiClientForUsername'); describe('Knowledge base user instructions', () => { @@ -94,62 +95,69 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('"editor" can retrieve their own private instructions and the public instruction', async () => { - const res = await observabilityAIAssistantAPIClient.editor({ - endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', - }); + await retry.try(async () => { + const res = await observabilityAIAssistantAPIClient.editor({ + endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', + }); - const instructions = res.body.userInstructions; + const instructions = res.body.userInstructions; + expect(instructions).to.have.length(3); - const sortById = (data: Array) => sortBy(data, 'id'); + const sortById = (data: Array) => sortBy(data, 'id'); - expect(sortById(instructions)).to.eql( - sortById([ - { - id: 'private-doc-from-editor', - public: false, - text: 'Private user instruction from "editor"', - }, - { - id: 'public-doc-from-editor', - public: true, - text: 'Public user instruction from "editor"', - }, - { - id: 'public-doc-from-secondary_editor', - public: true, - text: 'Public user instruction from "secondary_editor"', - }, - ]) - ); + expect(sortById(instructions)).to.eql( + sortById([ + { + id: 'private-doc-from-editor', + public: false, + text: 'Private user instruction from "editor"', + }, + { + id: 'public-doc-from-editor', + public: true, + text: 'Public user instruction from "editor"', + }, + { + id: 'public-doc-from-secondary_editor', + public: true, + text: 'Public user instruction from "secondary_editor"', + }, + ]) + ); + }); }); it('"secondaryEditor" can retrieve their own private instructions and the public instruction', async () => { - const res = await observabilityAIAssistantAPIClient.secondaryEditor({ - endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', - }); - const instructions = res.body.userInstructions; + await retry.try(async () => { + const res = await observabilityAIAssistantAPIClient.secondaryEditor({ + endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', + }); - const sortById = (data: Array) => sortBy(data, 'id'); + const instructions = res.body.userInstructions; + expect(instructions).to.have.length(3); - expect(sortById(instructions)).to.eql( - sortById([ - { - id: 'public-doc-from-editor', - public: true, - text: 'Public user instruction from "editor"', - }, - { - id: 'public-doc-from-secondary_editor', - public: true, - text: 'Public user instruction from "secondary_editor"', - }, - { - id: 'private-doc-from-secondary_editor', - public: false, - text: 'Private user instruction from "secondary_editor"', - }, - ]) - ); + const sortById = (data: Array) => sortBy(data, 'id'); + + expect(sortById(instructions)).to.eql( + sortById([ + { + id: 'public-doc-from-editor', + public: true, + text: 'Public user instruction from "editor"', + }, + { + id: 'public-doc-from-secondary_editor', + public: true, + text: 'Public user instruction from "secondary_editor"', + }, + { + id: 'private-doc-from-secondary_editor', + public: false, + text: 'Private user instruction from "secondary_editor"', + }, + ]) + ); + }); }); }); From 20953fcb6f9476a5dfa89b581ee65a59c319a8f3 Mon Sep 17 00:00:00 2001 From: wajihaparvez Date: Thu, 14 Nov 2024 12:02:01 -0500 Subject: [PATCH 06/16] [Docs] Add breaking change to release notes (#200065) ## Summary Adding a Kibana-related breaking change found in the Security release notes. --- docs/CHANGELOG.asciidoc | 17 +++++++++++++++-- docs/upgrade-notes.asciidoc | 9 +++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 0c9bd6ada3904..1b45c8700d9a3 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -156,9 +156,22 @@ The Observability AI Assistant specific advanced setting for Logs index patterns //*Impact* + //!!TODO!! ==== - - +[float] +[[breaking-changes-8.16.0]] +=== Breaking changes + +Breaking changes can prevent your application from optimal operation and performance. +Before you upgrade to 8.16.0, review the breaking changes, then mitigate the impact to your application. + +[discrete] +.Updated request processing during shutdown. +[%collapsible] +==== +*Details* + +During shutdown, {kib} now waits for all the ongoing requests to complete according to the `server.shutdownTimeout` setting. During that period, the incoming socket is closed and any new incoming requests are rejected. Before this update, new incoming requests received a response with the status code 503 and body `{"message": "Kibana is shutting down and not accepting new incoming requests"}`. For more information, refer to {kibana-pull}180986[#180986]. +==== + [float] [[features-8.16.0]] === Features diff --git a/docs/upgrade-notes.asciidoc b/docs/upgrade-notes.asciidoc index a0c2d6c1afccb..c2d866f90eed3 100644 --- a/docs/upgrade-notes.asciidoc +++ b/docs/upgrade-notes.asciidoc @@ -434,6 +434,15 @@ The endpoint is now split into two separate endpoints: // General settings +[discrete] +[[breaking-180986]] +.[General settings] Updated request processing during shutdown. (8.16) +[%collapsible] +==== +*Details* + +During shutdown, {kib} now waits for all the ongoing requests to complete according to the `server.shutdownTimeout` setting. During that period, the incoming socket is closed and any new incoming requests are rejected. Before this update, new incoming requests received a response with the status code 503 and body `{"message": "Kibana is shutting down and not accepting new incoming requests"}`. For more information, refer to {kibana-pull}180986[#180986]. +==== + [discrete] [[breaking-111535]] .[General settings] Removed `CONFIG_PATH` and `DATA_PATH` environment variables. (8.0) From e2702ff5912ec440060d62fb323a9a03c4881143 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 14 Nov 2024 18:05:11 +0100 Subject: [PATCH 07/16] [ResponseOps] [Cases] Attach file to case API (#198377) Fixes #22832 ## Summary This PR adds the possibility of adding Files/Attachments to Case in Kibana via an API call. ### How to test The new API URL is `https://localhost:5601/api/cases//files`. You can either use postman or curl to test. 1. Start by creating a case. 2. Call the new API ``` curl --location 'https://localhost:5601/api/cases//files' \ --header 'kbn-xsrf: true' \ --header 'Authorization: Basic ZWxhc3RpYzpjaGFuZ2VtZQ==' \ --form 'filename="Notice"' \ --form 'mimeType="text/plain"' \ --form 'file=@""' ``` Screenshot 2024-10-30 at 15 41 26 3. Confirm the user action was created. Screenshot 2024-10-30 at 15 48 45 4. Confirm the file exists in the case and: - it can be downloaded as expected. - it can be previewed as expected(not every MIME type allows this). ### Release Notes Files can now be attached to cases directly via API. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: lcawl --- oas_docs/output/kibana.yaml | 59 ++++++ .../plugins/cases/common/constants/index.ts | 3 + .../plugins/cases/common/schema/index.test.ts | 27 ++- x-pack/plugins/cases/common/schema/index.ts | 15 ++ .../common/types/api/attachment/v1.test.ts | 52 ++++- .../cases/common/types/api/attachment/v1.ts | 17 ++ .../common/types/domain/attachment/v1.ts | 7 + .../plugins/cases/docs/openapi/bundled.json | 74 ++++++++ .../plugins/cases/docs/openapi/bundled.yaml | 49 +++++ .../schemas/add_case_file_request.yaml | 13 ++ .../cases/docs/openapi/entrypoint.yaml | 72 +++---- .../paths/api@cases@{caseid}@files.yaml | 40 ++++ .../client/attachments/add_file.test.ts | 129 +++++++++++++ .../server/client/attachments/add_file.ts | 109 +++++++++++ .../cases/server/client/attachments/client.ts | 7 + .../cases/server/client/attachments/types.ts | 36 +++- x-pack/plugins/cases/server/client/mocks.ts | 1 + .../plugins/cases/server/client/utils.test.ts | 38 ++++ x-pack/plugins/cases/server/client/utils.ts | 31 +++ .../server/routes/api/files/post_file.test.ts | 115 +++++++++++ .../server/routes/api/files/post_file.ts | 92 +++++++++ .../server/routes/api/get_external_routes.ts | 2 + .../common/lib/api/attachments.ts | 28 +++ .../cases_api_integration/common/lib/mock.ts | 10 +- .../tests/common/cases/delete_cases.ts | 3 + .../tests/common/files/post_file.ts | 179 ++++++++++++++++++ .../security_and_spaces/tests/common/index.ts | 1 + .../tests/common/files/post_file.ts | 86 +++++++++ .../spaces_only/tests/common/index.ts | 1 + 29 files changed, 1254 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/cases/docs/openapi/components/schemas/add_case_file_request.yaml create mode 100644 x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@files.yaml create mode 100644 x-pack/plugins/cases/server/client/attachments/add_file.test.ts create mode 100644 x-pack/plugins/cases/server/client/attachments/add_file.ts create mode 100644 x-pack/plugins/cases/server/routes/api/files/post_file.test.ts create mode 100644 x-pack/plugins/cases/server/routes/api/files/post_file.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/files/post_file.ts create mode 100644 x-pack/test/cases_api_integration/spaces_only/tests/common/files/post_file.ts diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 82acbdb311f2f..ceefaa13fcd4b 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -6706,6 +6706,46 @@ paths: summary: Push a case to an external service tags: - cases + /api/cases/{caseId}/files: + post: + description: > + Attach a file to a case. You must have `all` privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case you're updating. The request must include: + + - The `Content-Type: multipart/form-data` HTTP header. + + - The location of the file that is being uploaded. + operationId: addCaseFileDefaultSpace + parameters: + - $ref: '#/components/parameters/Cases_kbn_xsrf' + - $ref: '#/components/parameters/Cases_case_id' + requestBody: + content: + multipart/form-data; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Cases_add_case_file_request' + required: true + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + addCaseFileResponse: + $ref: '#/components/examples/Cases_add_comment_response' + schema: + $ref: '#/components/schemas/Cases_case_response_properties' + description: Indicates a successful call. + '401': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Cases_4xx_response' + description: Authorization information is missing or invalid. + summary: Attach a file to a case + tags: + - cases /api/cases/{caseId}/user_actions: get: deprecated: true @@ -43674,6 +43714,25 @@ components: - $ref: '#/components/schemas/Cases_add_alert_comment_request_properties' - $ref: '#/components/schemas/Cases_add_user_comment_request_properties' title: Add case comment request + Cases_add_case_file_request: + description: >- + Defines the file that will be attached to the case. Optional parameters + will be generated automatically from the file metadata if not defined. + type: object + properties: + file: + description: The file being attached to the case. + format: binary + type: string + filename: + description: >- + The desired name of the file being attached to the case, it can be + different than the name of the file in the filesystem. **This should + not include the file extension.** + type: string + required: + - file + title: Add case file request properties Cases_add_user_comment_request_properties: description: Defines properties for case comment requests when type is user. properties: diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index 557899e322ae7..aa3855807cea2 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -62,6 +62,8 @@ export const CASE_FIND_USER_ACTIONS_URL = `${CASE_USER_ACTIONS_URL}/_find` as co export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}` as const; export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const; +export const CASE_FILES_URL = `${CASE_DETAILS_URL}/files` as const; + /** * Internal routes */ @@ -139,6 +141,7 @@ export const MAX_TEMPLATE_DESCRIPTION_LENGTH = 1000 as const; export const MAX_TEMPLATES_LENGTH = 10 as const; export const MAX_TEMPLATE_TAG_LENGTH = 50 as const; export const MAX_TAGS_PER_TEMPLATE = 10 as const; +export const MAX_FILENAME_LENGTH = 160 as const; /** * Cases features diff --git a/x-pack/plugins/cases/common/schema/index.test.ts b/x-pack/plugins/cases/common/schema/index.test.ts index 64eb2ad393fcb..8a20943831cd0 100644 --- a/x-pack/plugins/cases/common/schema/index.test.ts +++ b/x-pack/plugins/cases/common/schema/index.test.ts @@ -11,6 +11,7 @@ import { limitedArraySchema, limitedNumberSchema, limitedStringSchema, + mimeTypeString, NonEmptyString, paginationSchema, limitedNumberAsIntegerSchema, @@ -321,14 +322,32 @@ describe('schema', () => { }); }); + describe('mimeTypeString', () => { + it('works correctly when the value is an allowed mime type', () => { + expect(PathReporter.report(mimeTypeString.decode('image/jpx'))).toMatchInlineSnapshot(` + Array [ + "No errors!", + ] + `); + }); + + it('fails when the value is not an allowed mime type', () => { + expect(PathReporter.report(mimeTypeString.decode('foo/bar'))).toMatchInlineSnapshot(` + Array [ + "The mime type field value foo/bar is not allowed.", + ] + `); + }); + }); + describe('limitedNumberAsIntegerSchema', () => { it('works correctly the number is safe integer', () => { expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1))) .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + Array [ + "No errors!", + ] + `); }); it('fails when given a number that is lower than the minimum', () => { diff --git a/x-pack/plugins/cases/common/schema/index.ts b/x-pack/plugins/cases/common/schema/index.ts index 0bcbdcfb2c480..1ddd4a161b6e6 100644 --- a/x-pack/plugins/cases/common/schema/index.ts +++ b/x-pack/plugins/cases/common/schema/index.ts @@ -11,6 +11,7 @@ import { either } from 'fp-ts/lib/Either'; import { MAX_DOCS_PER_PAGE } from '../constants'; import type { PartialPaginationType } from './types'; import { PaginationSchemaRt } from './types'; +import { ALLOWED_MIME_TYPES } from '../constants/mime_types'; export interface LimitedSchemaType { fieldName: string; @@ -194,3 +195,17 @@ export const regexStringRt = ({ codec, pattern, message }: RegexStringSchemaType }), rt.identity ); + +export const mimeTypeString = new rt.Type( + 'mimeTypeString', + rt.string.is, + (input, context) => + either.chain(rt.string.validate(input, context), (s) => { + if (!ALLOWED_MIME_TYPES.includes(s)) { + return rt.failure(input, context, `The mime type field value ${s} is not allowed.`); + } + + return rt.success(s); + }), + rt.identity +); diff --git a/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts b/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts index e08cff94717e4..28c0b5d8da789 100644 --- a/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts +++ b/x-pack/plugins/cases/common/types/api/attachment/v1.test.ts @@ -6,7 +6,11 @@ */ import { PathReporter } from 'io-ts/lib/PathReporter'; -import { MAX_BULK_CREATE_ATTACHMENTS, MAX_COMMENT_LENGTH } from '../../../constants'; +import { + MAX_BULK_CREATE_ATTACHMENTS, + MAX_COMMENT_LENGTH, + MAX_FILENAME_LENGTH, +} from '../../../constants'; import { AttachmentType } from '../../domain/attachment/v1'; import { AttachmentPatchRequestRt, @@ -17,6 +21,7 @@ import { BulkGetAttachmentsRequestRt, BulkGetAttachmentsResponseRt, FindAttachmentsQueryParamsRt, + PostFileAttachmentRequestRt, } from './v1'; describe('Attachments', () => { @@ -389,4 +394,49 @@ describe('Attachments', () => { }); }); }); + + describe('PostFileAttachmentRequestRt', () => { + const defaultRequest = { + file: 'Solve this fast!', + filename: 'filename', + }; + + it('has the expected attributes in request', () => { + const query = PostFileAttachmentRequestRt.decode(defaultRequest); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it('removes foo:bar attributes from request', () => { + const query = PostFileAttachmentRequestRt.decode({ ...defaultRequest, foo: 'bar' }); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + describe('errors', () => { + it('throws an error when the filename is too long', () => { + const longFilename = 'x'.repeat(MAX_FILENAME_LENGTH + 1); + + expect( + PathReporter.report( + PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: longFilename }) + ) + ).toContain('The length of the filename is too long. The maximum length is 160.'); + }); + + it('throws an error when the filename is too small', () => { + expect( + PathReporter.report( + PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: '' }) + ) + ).toContain('The filename field cannot be an empty string.'); + }); + }); + }); }); diff --git a/x-pack/plugins/cases/common/types/api/attachment/v1.ts b/x-pack/plugins/cases/common/types/api/attachment/v1.ts index 3d9cb02a5ce55..a6a2c3f97b39e 100644 --- a/x-pack/plugins/cases/common/types/api/attachment/v1.ts +++ b/x-pack/plugins/cases/common/types/api/attachment/v1.ts @@ -12,6 +12,7 @@ import { MAX_COMMENTS_PER_PAGE, MAX_COMMENT_LENGTH, MAX_DELETE_FILES, + MAX_FILENAME_LENGTH, } from '../../../constants'; import { limitedArraySchema, @@ -47,7 +48,23 @@ export const BulkDeleteFileAttachmentsRequestRt = rt.strict({ }), }); +export const PostFileAttachmentRequestRt = rt.intersection([ + rt.strict({ + file: rt.unknown, + }), + rt.exact( + rt.partial({ + filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }), + }) + ), +]); + export type BulkDeleteFileAttachmentsRequest = rt.TypeOf; +export type PostFileAttachmentRequest = rt.TypeOf; + +/** + * Attachments + */ const BasicAttachmentRequestRt = rt.union([ UserCommentAttachmentPayloadRt, diff --git a/x-pack/plugins/cases/common/types/domain/attachment/v1.ts b/x-pack/plugins/cases/common/types/domain/attachment/v1.ts index df6193bd3ae36..1dbf1129f58c5 100644 --- a/x-pack/plugins/cases/common/types/domain/attachment/v1.ts +++ b/x-pack/plugins/cases/common/types/domain/attachment/v1.ts @@ -6,8 +6,10 @@ */ import * as rt from 'io-ts'; +import { limitedStringSchema, mimeTypeString } from '../../../schema'; import { jsonValueRt } from '../../../api'; import { UserRt } from '../user/v1'; +import { MAX_FILENAME_LENGTH } from '../../../constants'; /** * Files @@ -35,6 +37,11 @@ export const AttachmentAttributesBasicRt = rt.strict({ updated_by: rt.union([UserRt, rt.null]), }); +export const FileAttachmentMetadataPayloadRt = rt.strict({ + mimeType: mimeTypeString, + filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }), +}); + /** * User comment */ diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index 6b84ab8e090fa..aaf7b7afaec24 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -1869,6 +1869,61 @@ } } } + }, + "/api/cases/{caseId}/files": { + "post": { + "summary": "Attach a file to a case", + "operationId": "addCaseFileDefaultSpace", + "description": "Attach a file to a case. You must have `all` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. The request must include:\n- The `Content-Type: multipart/form-data` HTTP header.\n- The location of the file that is being uploaded.\n", + "tags": [ + "cases" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/case_id" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/add_case_file_request" + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/case_response_properties" + }, + "examples": { + "addCaseFileResponse": { + "$ref": "#/components/examples/add_comment_response" + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/4xx_response" + } + } + } + } + } + } } }, "components": { @@ -4798,6 +4853,25 @@ "example": "create_case" } } + }, + "add_case_file_request": { + "title": "Add case file request properties", + "required": [ + "file" + ], + "description": "Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined.", + "type": "object", + "properties": { + "file": { + "description": "The file being attached to the case.", + "type": "string", + "format": "binary" + }, + "filename": { + "description": "The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.**", + "type": "string" + } + } } }, "examples": { diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index 40c86d41e1b26..5cfcca36f309a 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -1208,6 +1208,41 @@ paths: application/json: schema: $ref: '#/components/schemas/4xx_response' + /api/cases/{caseId}/files: + post: + summary: Attach a file to a case + operationId: addCaseFileDefaultSpace + description: | + Attach a file to a case. You must have `all` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. The request must include: + - The `Content-Type: multipart/form-data` HTTP header. + - The location of the file that is being uploaded. + tags: + - cases + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/case_id' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/add_case_file_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/case_response_properties' + examples: + addCaseFileResponse: + $ref: '#/components/examples/add_comment_response' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/4xx_response' components: parameters: kbn_xsrf: @@ -3331,6 +3366,20 @@ components: - settings - severity example: create_case + add_case_file_request: + title: Add case file request properties + required: + - file + description: Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined. + type: object + properties: + file: + description: The file being attached to the case. + type: string + format: binary + filename: + description: The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.** + type: string examples: create_case_request: summary: Create a security case that uses a Jira connector. diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/add_case_file_request.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/add_case_file_request.yaml new file mode 100644 index 0000000000000..d114a7ba1be47 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/add_case_file_request.yaml @@ -0,0 +1,13 @@ +title: Add case file request properties +required: + - file +description: Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined. +type: object +properties: + file: + description: The file being attached to the case. + type: string + format: binary + filename: + description: The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.** + type: string diff --git a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml index 764de3e6570fb..09280d6f33ff9 100644 --- a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml @@ -14,7 +14,7 @@ tags: servers: - url: / paths: -# Paths in the default space + # Paths in the default space '/api/cases': $ref: 'paths/api@cases.yaml' '/api/cases/_find': @@ -49,41 +49,43 @@ paths: $ref: 'paths/api@cases@{caseid}@user_actions@_find.yaml' '/api/cases/configure/connectors/_find': $ref: paths/api@cases@configure@connectors@_find.yaml + '/api/cases/{caseId}/files': + $ref: 'paths/api@cases@{caseid}@files.yaml' # Paths with space identifiers - # '/s/{spaceId}/api/cases': - # $ref: 'paths/s@{spaceid}@api@cases.yaml' - # '/s/{spaceId}/api/cases/_find': - # $ref: 'paths/s@{spaceid}@api@cases@_find.yaml' - # '/s/{spaceId}/api/cases/alerts/{alertId}': - # $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' - # '/s/{spaceId}/api/cases/configure': - # $ref: paths/s@{spaceid}@api@cases@configure.yaml - # '/s/{spaceId}/api/cases/configure/{configurationId}': - # $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml - # '/s/{spaceId}/api/cases/configure/connectors/_find': - # $ref: paths/s@{spaceid}@api@cases@configure@connectors@_find.yaml - # '/s/{spaceId}/api/cases/reporters': - # $ref: 'paths/s@{spaceid}@api@cases@reporters.yaml' - # '/s/{spaceId}/api/cases/status': - # $ref: 'paths/s@{spaceid}@api@cases@status.yaml' - # '/s/{spaceId}/api/cases/tags': - # $ref: 'paths/s@{spaceid}@api@cases@tags.yaml' - # '/s/{spaceId}/api/cases/{caseId}': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}.yaml' - # '/s/{spaceId}/api/cases/{caseId}/alerts': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml' - # '/s/{spaceId}/api/cases/{caseId}/comments': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml' - # '/s/{spaceId}/api/cases/{caseId}/comments/_find': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@_find.yaml' - # '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' - # '/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml' - # '/s/{spaceId}/api/cases/{caseId}/user_actions': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions.yaml' - # '/s/{spaceId}/api/cases/{caseId}/user_actions/_find': - # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml' +# '/s/{spaceId}/api/cases': +# $ref: 'paths/s@{spaceid}@api@cases.yaml' +# '/s/{spaceId}/api/cases/_find': +# $ref: 'paths/s@{spaceid}@api@cases@_find.yaml' +# '/s/{spaceId}/api/cases/alerts/{alertId}': +# $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' +# '/s/{spaceId}/api/cases/configure': +# $ref: paths/s@{spaceid}@api@cases@configure.yaml +# '/s/{spaceId}/api/cases/configure/{configurationId}': +# $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml +# '/s/{spaceId}/api/cases/configure/connectors/_find': +# $ref: paths/s@{spaceid}@api@cases@configure@connectors@_find.yaml +# '/s/{spaceId}/api/cases/reporters': +# $ref: 'paths/s@{spaceid}@api@cases@reporters.yaml' +# '/s/{spaceId}/api/cases/status': +# $ref: 'paths/s@{spaceid}@api@cases@status.yaml' +# '/s/{spaceId}/api/cases/tags': +# $ref: 'paths/s@{spaceid}@api@cases@tags.yaml' +# '/s/{spaceId}/api/cases/{caseId}': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}.yaml' +# '/s/{spaceId}/api/cases/{caseId}/alerts': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml' +# '/s/{spaceId}/api/cases/{caseId}/comments': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml' +# '/s/{spaceId}/api/cases/{caseId}/comments/_find': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@_find.yaml' +# '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' +# '/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml' +# '/s/{spaceId}/api/cases/{caseId}/user_actions': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions.yaml' +# '/s/{spaceId}/api/cases/{caseId}/user_actions/_find': +# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml' # components: # securitySchemes: # basicAuth: diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@files.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@files.yaml new file mode 100644 index 0000000000000..751e61cd75402 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@files.yaml @@ -0,0 +1,40 @@ +post: + summary: Attach a file to a case + operationId: addCaseFileDefaultSpace + description: > + Attach a file to a case. + You must have `all` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security** section of the Kibana + feature privileges, depending on the owner of the case you're updating. + The request must include: + + - The `Content-Type: multipart/form-data` HTTP header. + + - The location of the file that is being uploaded. + tags: + - cases + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/case_id.yaml' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '../components/schemas/add_case_file_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + addCaseFileResponse: + $ref: '../components/examples/add_comment_response.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/4xx_response.yaml' diff --git a/x-pack/plugins/cases/server/client/attachments/add_file.test.ts b/x-pack/plugins/cases/server/client/attachments/add_file.test.ts new file mode 100644 index 0000000000000..16f803389f54a --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/add_file.test.ts @@ -0,0 +1,129 @@ +/* + * 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 { Readable } from 'stream'; +import type { File } from '@kbn/files-plugin/common'; + +import type { Case } from '../../../common'; +import { MAX_USER_ACTIONS_PER_CASE } from '../../../common/constants'; +import { createUserActionServiceMock } from '../../services/mocks'; +import { createCasesClientMock, createCasesClientMockArgs } from '../mocks'; +import { addFile } from './add_file'; +import { buildAttachmentRequestFromFileJSON } from '../utils'; + +jest.mock('../utils'); + +const buildAttachmentRequestFromFileJSONMock = buildAttachmentRequestFromFileJSON as jest.Mock; + +describe('addFile', () => { + const caseId = 'test-case'; + const file = new Readable(); + file.push('theFile'); + file.push(null); + const owner = 'cases'; + + const clientArgs = createCasesClientMockArgs(); + const casesClient = createCasesClientMock(); + const userActionService = createUserActionServiceMock(); + + clientArgs.services.userActionService = userActionService; + + beforeEach(() => { + jest.clearAllMocks(); + userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({}); + casesClient.cases.get.mockResolvedValue({ id: caseId, owner } as unknown as Case); + }); + + it('throws an error if the filename is missing', async () => { + await expect( + addFile( + // @ts-expect-error + { caseId, fileRequest: { file, mimeType: 'text/plain' } }, + clientArgs, + casesClient + ) + ).rejects.toThrow(`Invalid value "undefined" supplied to "filename"`); + }); + + it('throws an error if the mimeType is not part of the allowed mime types', async () => { + await expect( + addFile({ caseId, file, filename: 'foobar', mimeType: 'foo/bar' }, clientArgs, casesClient) + ).rejects.toThrow('The mime type field value foo/bar is not allowed.'); + }); + + it(`throws an error when the case user actions become > ${MAX_USER_ACTIONS_PER_CASE}`, async () => { + userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({ + [caseId]: MAX_USER_ACTIONS_PER_CASE, + }); + + await expect( + addFile({ caseId, file, filename: 'foobar', mimeType: 'text/plain' }, clientArgs, casesClient) + ).rejects.toThrow( + `The case with id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.` + ); + }); + + it('calls fileService.delete when an error is thrown after the file was created', async () => { + const id = 'file-id'; + + clientArgs.fileService.create.mockResolvedValue({ + id, + uploadContent: jest.fn(), + toJSON: () => { + throw new Error(); // ensures an error is thrown after file creation + }, + } as unknown as File); + + await expect( + addFile( + { + caseId, + file, + filename: 'foobar', + mimeType: 'text/plain', + }, + clientArgs, + casesClient + ) + ).rejects.toThrowError(); + expect(clientArgs.fileService.delete).toHaveBeenCalledWith({ id }); + }); + + it('calls buildAttachmentRequestFromFileJSON with the correct params', async () => { + const fileMetadata = { + id: 'file-id', + created: 'created', + extension: 'jpg', + mimeType: 'image/jpeg', + name: 'foobar', + }; + + clientArgs.fileService.create.mockResolvedValue({ + id: fileMetadata.id, + uploadContent: jest.fn(), + toJSON: () => fileMetadata, + } as unknown as File); + + buildAttachmentRequestFromFileJSONMock.mockImplementation(() => { + throw new Error(); // ensures the test finishes early + }); + + await expect( + addFile( + { + caseId, + file, + filename: fileMetadata.name, + mimeType: fileMetadata.mimeType, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowError(); + expect(buildAttachmentRequestFromFileJSONMock).toHaveBeenCalledWith({ owner, fileMetadata }); + }); +}); diff --git a/x-pack/plugins/cases/server/client/attachments/add_file.ts b/x-pack/plugins/cases/server/client/attachments/add_file.ts new file mode 100644 index 0000000000000..5f3ee614e99ed --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/add_file.ts @@ -0,0 +1,109 @@ +/* + * 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 { SavedObjectsUtils } from '@kbn/core/server'; + +import type { Owner } from '../../../common/constants/types'; +import { FileAttachmentMetadataPayloadRt, type Case } from '../../../common/types/domain'; +import type { CasesClient, CasesClientArgs } from '..'; +import type { AddFileArgs } from './types'; + +import { CaseCommentModel } from '../../common/models'; +import { createCaseError } from '../../common/error'; +import { validateMaxUserActions } from '../../common/validators'; +import { constructFileKindIdByOwner } from '../../../common/files'; +import { Operations } from '../../authorization'; +import { validateRegisteredAttachments } from './validators'; +import { buildAttachmentRequestFromFileJSON } from '../utils'; +import { decodeWithExcessOrThrow } from '../../common/runtime_types'; + +/** + * Create a file attachment to a case. + */ +export const addFile = async ( + addFileArgs: AddFileArgs, + clientArgs: CasesClientArgs, + casesClient: CasesClient +): Promise => { + const { caseId, file, filename, mimeType, $abort } = addFileArgs; + const { + logger, + authorization, + persistableStateAttachmentTypeRegistry, + externalReferenceAttachmentTypeRegistry, + services: { userActionService }, + fileService, + } = clientArgs; + + let createdFile; + + try { + decodeWithExcessOrThrow(FileAttachmentMetadataPayloadRt)({ + filename, + mimeType, + }); + + // This will perform an authorization check to ensure the user has access to the parent case + const theCase = await casesClient.cases.get({ + id: caseId, + includeComments: false, + }); + + const owner = theCase.owner; + + await validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 }); + + const savedObjectID = SavedObjectsUtils.generateId(); + + await authorization.ensureAuthorized({ + operation: Operations.createComment, + entities: [{ owner, id: savedObjectID }], + }); + + createdFile = await fileService.create({ + name: filename, + mime: mimeType, + fileKind: constructFileKindIdByOwner(owner as Owner), + meta: { caseIds: [caseId], owner: [owner] }, + }); + + await createdFile.uploadContent(file, $abort); + + const commentReq = buildAttachmentRequestFromFileJSON({ + owner, + fileMetadata: createdFile.toJSON(), + }); + + validateRegisteredAttachments({ + query: commentReq, + persistableStateAttachmentTypeRegistry, + externalReferenceAttachmentTypeRegistry, + }); + + const createdDate = new Date().toISOString(); + + const model = await CaseCommentModel.create(caseId, clientArgs); + + const updatedModel = await model.createComment({ + createdDate, + commentReq, + id: savedObjectID, + }); + + return await updatedModel.encodeWithComments(); + } catch (error) { + if (createdFile?.id) { + await fileService.delete({ id: createdFile.id }); + } + + throw createCaseError({ + message: `Failed while adding a comment to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index 9c455cbe750c3..61586092e69b9 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -28,6 +28,7 @@ import type { UpdateArgs, BulkGetArgs, BulkDeleteFileArgs, + AddFileArgs, } from './types'; import { bulkCreate } from './bulk_create'; import { deleteAll, deleteComment } from './delete'; @@ -35,6 +36,7 @@ import { find, get, getAll, getAllAlertsAttachToCase } from './get'; import { bulkGet } from './bulk_get'; import { update } from './update'; import { bulkDeleteFileAttachments } from './bulk_delete'; +import { addFile } from './add_file'; /** * API for interacting with the attachments to a case. @@ -77,6 +79,10 @@ export interface AttachmentsSubClient { * The request must include all fields for the attachment. Even the fields that are not changing. */ update(updateArgs: UpdateArgs): Promise; + /** + * Adds a file attachment to a case. Returns the case with comments. + */ + addFile(params: AddFileArgs): Promise; } /** @@ -102,6 +108,7 @@ export const createAttachmentsSubClient = ( getAll: (params) => getAll(params, clientArgs), get: (params) => get(params, clientArgs), update: (params) => update(params, clientArgs), + addFile: (params: AddFileArgs) => addFile(params, clientArgs, casesClient), }; return Object.freeze(attachmentSubClient); diff --git a/x-pack/plugins/cases/server/client/attachments/types.ts b/x-pack/plugins/cases/server/client/attachments/types.ts index 0dd700a813110..bc9abed0d0da0 100644 --- a/x-pack/plugins/cases/server/client/attachments/types.ts +++ b/x-pack/plugins/cases/server/client/attachments/types.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { Readable } from 'stream'; +import type { ReplaySubject } from 'rxjs'; import type { BulkCreateAttachmentsRequest, AttachmentPatchRequest, @@ -132,3 +133,36 @@ export interface UpdateArgs { */ updateRequest: AttachmentPatchRequest; } + +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + headers: Record; + }; +} + +/** + * The arguments needed for attaching a file to a case. + */ +export interface AddFileArgs { + /** + * The case ID that this attachment will be associated with + */ + caseId: string; + /** + * The file to upload + */ + file: Readable; + /** + * The name of the file to upload + */ + filename: string; + /** + * The mime type of the file to upload + */ + mimeType?: string; + /** + * An observable that can be used to abort the upload at any time. + */ + $abort?: ReplaySubject; +} diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 74d3c3de46fa0..50dca1920b625 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -91,6 +91,7 @@ const createAttachmentsSubClientMock = (): AttachmentsSubClientMock => { return { bulkGet: jest.fn(), add: jest.fn(), + addFile: jest.fn(), bulkCreate: jest.fn(), delete: jest.fn(), deleteAll: jest.fn(), diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 680887b82c653..7b5d692f6aa79 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -14,6 +14,7 @@ import { toElasticsearchQuery, toKqlExpression } from '@kbn/es-query'; import { createSavedObjectsSerializerMock } from './mocks'; import { arraysDifference, + buildAttachmentRequestFromFileJSON, buildFilter, buildRangeFilter, constructQueryOptions, @@ -24,6 +25,7 @@ import { import { CasePersistedSeverity, CasePersistedStatus } from '../common/types/case'; import type { CustomFieldsConfiguration } from '../../common/types/domain'; import { CaseSeverity, CaseStatuses, CustomFieldTypes } from '../../common/types/domain'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; describe('utils', () => { describe('buildFilter', () => { @@ -1576,4 +1578,40 @@ describe('utils', () => { expect(res).toEqual([]); }); }); + + describe('buildAttachmentRequestFromFileJSON', () => { + it('builds attachment request correctly', () => { + expect( + buildAttachmentRequestFromFileJSON({ + owner: 'theOwner', + fileMetadata: { + id: 'file-id', + created: 'created', + extension: 'jpg', + mimeType: 'image/jpeg', + name: 'foobar', + } as FileJSON, + }) + ).toStrictEqual({ + externalReferenceAttachmentTypeId: '.files', + externalReferenceId: 'file-id', + externalReferenceMetadata: { + files: [ + { + created: 'created', + extension: 'jpg', + mimeType: 'image/jpeg', + name: 'foobar', + }, + ], + }, + externalReferenceStorage: { + soType: 'file', + type: 'savedObject', + }, + owner: 'theOwner', + type: 'externalReference', + }); + }); + }); }); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 5f854aa3236fe..6447b53abd00a 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -16,6 +16,8 @@ import type { KueryNode } from '@kbn/es-query'; import { nodeBuilder, fromKueryExpression, escapeKuery } from '@kbn/es-query'; import { spaceIdToNamespace } from '@kbn/spaces-plugin/server/lib/utils/namespace'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import { FILE_SO_TYPE } from '@kbn/files-plugin/common/constants'; import type { CaseCustomField, CaseSeverity, @@ -28,6 +30,7 @@ import type { import { ActionsAttachmentPayloadRt, AlertAttachmentPayloadRt, + AttachmentType, ExternalReferenceNoSOAttachmentPayloadRt, ExternalReferenceSOAttachmentPayloadRt, ExternalReferenceStorageType, @@ -40,6 +43,7 @@ import type { CasesSearchParams } from './types'; import { decodeWithExcessOrThrow } from '../common/runtime_types'; import { CASE_SAVED_OBJECT, + FILE_ATTACHMENT_TYPE, NO_ASSIGNEES_FILTERING_KEYWORD, OWNER_FIELD, } from '../../common/constants'; @@ -660,3 +664,30 @@ export const transformTemplateCustomFields = ({ }; }); }; + +export const buildAttachmentRequestFromFileJSON = ({ + owner, + fileMetadata, +}: { + owner: string; + fileMetadata: FileJSON; +}): AttachmentRequest => ({ + owner, + type: AttachmentType.externalReference, + externalReferenceId: fileMetadata.id, + externalReferenceStorage: { + type: ExternalReferenceStorageType.savedObject, + soType: FILE_SO_TYPE, + }, + externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE, + externalReferenceMetadata: { + files: [ + { + name: fileMetadata.name, + extension: fileMetadata.extension ?? 'txt', + mimeType: fileMetadata.mimeType ?? 'text/plain', + created: fileMetadata.created, + }, + ], + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/files/post_file.test.ts b/x-pack/plugins/cases/server/routes/api/files/post_file.test.ts new file mode 100644 index 0000000000000..5fb2eb581320e --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/files/post_file.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { createCasesClientMock } from '../../../client/mocks'; +import { postFileRoute } from './post_file'; + +describe('getCaseRoute', () => { + const casesClientMock = createCasesClientMock(); + const response = { ok: jest.fn() }; + const context = { cases: { getCasesClient: jest.fn().mockResolvedValue(casesClientMock) } }; + const sub = { unsubscribe: jest.fn() }; + + afterEach(() => jest.clearAllMocks()); + + it('extracts the file metadata from hapi as expected', async () => { + const request = { + body: { file: { hapi: { filename: 'foobar.txt' } } }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: { + hapi: { + filename: 'foobar.txt', + }, + }, + filename: 'foobar', + mimeType: 'text/plain', + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('filename in body takes precedence over metadata', async () => { + const request = { + body: { file: { hapi: { filename: 'foobar.txt' } }, filename: 'foo' }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: { + hapi: { + filename: 'foobar.txt', + }, + }, + filename: 'foo', + mimeType: 'text/plain', + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('unrecognized mimetype will be sent as undefined', async () => { + const request = { + body: { file: { hapi: { filename: 'foobar.foobar' } } }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: { + hapi: { + filename: 'foobar.foobar', + }, + }, + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('missing hapi will not throw an error', async () => { + const request = { + body: { file: {} }, + events: { aborted$: { subscribe: jest.fn().mockReturnValue(sub) } }, + params: { case_id: 'bar' }, + }; + + // @ts-ignore + await postFileRoute.handler({ context, request, response }); + + expect(casesClientMock.attachments.addFile).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: 'bar', + file: {}, + filename: '', + }) + ); + + expect(sub.unsubscribe).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/server/routes/api/files/post_file.ts b/x-pack/plugins/cases/server/routes/api/files/post_file.ts new file mode 100644 index 0000000000000..2d7b5b6318a4b --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/files/post_file.ts @@ -0,0 +1,92 @@ +/* + * 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 Boom from '@hapi/boom'; +import mime from 'mime-types'; +import { parse } from 'path'; + +import { schema } from '@kbn/config-schema'; +import { ReplaySubject } from 'rxjs'; + +import { AbortedUploadError } from '@kbn/files-plugin/server/file/errors'; +import type { HapiReadableStream } from '../../../client/attachments/types'; +import type { attachmentApiV1 } from '../../../../common/types/api'; + +import { CASE_FILES_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; +import type { caseDomainV1 } from '../../../../common/types/domain'; + +export const postFileRoute = createCasesRoute({ + method: 'post', + path: CASE_FILES_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + routerOptions: { + access: 'public', + summary: 'Attach a file to a case', + tags: ['oas-tag:cases'], + body: { + // This is set to 10 GiB as an upper boundary on the size of the HTTP request body. + // The file service will throw 413 errors if the file size is larger than expected. + maxBytes: 10 * 1024 * 1024 * 1024, + output: 'stream', + parse: true, + accepts: 'multipart/form-data', + }, + }, + handler: async ({ context, request, response }) => { + const $abort = new ReplaySubject(); + const sub = request.events.aborted$.subscribe($abort); + + try { + const caseContext = await context.cases; + const casesClient = await caseContext.getCasesClient(); + + const fileRequest = request.body as attachmentApiV1.PostFileAttachmentRequest; + const file = fileRequest.file as HapiReadableStream; + + let filename = fileRequest.filename; + let mimeType; + + if (file.hapi != null) { + const parsedFilename = parse(file.hapi.filename); + + filename ??= parsedFilename.name; + mimeType = mime.lookup(parsedFilename.ext.toLowerCase()) || undefined; + } + + const res: caseDomainV1.Case = await casesClient.attachments.addFile({ + file, + filename: filename ?? '', + mimeType, + caseId: request.params.case_id, + $abort, + }); + + return response.ok({ + body: res, + }); + } catch (error) { + if (error instanceof AbortedUploadError) { + throw new Boom.Boom(error, { + statusCode: 499, + message: error.message, + }); + } + throw createCaseError({ + message: `Failed to attach file to case in route: ${error}`, + error, + }); + } finally { + sub.unsubscribe(); + } + }, +}); diff --git a/x-pack/plugins/cases/server/routes/api/get_external_routes.ts b/x-pack/plugins/cases/server/routes/api/get_external_routes.ts index 4412d0b695079..b68a1c32a93f8 100644 --- a/x-pack/plugins/cases/server/routes/api/get_external_routes.ts +++ b/x-pack/plugins/cases/server/routes/api/get_external_routes.ts @@ -30,6 +30,7 @@ import { patchCaseConfigureRoute } from './configure/patch_configure'; import { postCaseConfigureRoute } from './configure/post_configure'; import { getAllAlertsAttachedToCaseRoute } from './comments/get_alerts'; import { findUserActionsRoute } from './user_actions/find_user_actions'; +import { postFileRoute } from './files/post_file'; export const getExternalRoutes = ({ isServerless }: { isServerless?: boolean }) => [ @@ -58,4 +59,5 @@ export const getExternalRoutes = ({ isServerless }: { isServerless?: boolean }) patchCaseConfigureRoute, postCaseConfigureRoute, getAllAlertsAttachedToCaseRoute, + postFileRoute, ] as CaseRoute[]; diff --git a/x-pack/test/cases_api_integration/common/lib/api/attachments.ts b/x-pack/test/cases_api_integration/common/lib/api/attachments.ts index 9c06f9b3efa08..7147a1fdf03ee 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/attachments.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/attachments.ts @@ -18,6 +18,7 @@ import { BulkCreateAttachmentsRequest, AttachmentPatchRequest, AttachmentsFindResponse, + PostFileAttachmentRequest, } from '@kbn/cases-plugin/common/types/api'; import { Attachments, Attachment } from '@kbn/cases-plugin/common/types/domain'; import { User } from '../authentication/types'; @@ -79,6 +80,33 @@ export const createComment = async ({ return theCase; }; +export const createFileAttachment = async ({ + supertest, + caseId, + params, + auth = { user: superUser, space: null }, + expectedHttpCode = 200, + headers = {}, +}: { + supertest: SuperTest.Agent; + caseId: string; + params: PostFileAttachmentRequest; + auth?: { user: User; space: string | null } | null; + expectedHttpCode?: number; + headers?: Record; +}): Promise => { + const apiCall = supertest.post(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/${caseId}/files`); + void setupAuth({ apiCall, headers, auth }); + + const { body: theCase } = await apiCall + .set('kbn-xsrf', 'true') + .set(headers) + .attach('file', Buffer.from(params.file as unknown as string), params.filename) + .expect(expectedHttpCode); + + return theCase; +}; + export const bulkCreateAttachments = async ({ supertest, caseId, diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 969df10a53d1d..2461cded5aeaa 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -20,7 +20,10 @@ import { PersistableStateAttachmentPayload, Attachment, } from '@kbn/cases-plugin/common/types/domain'; -import type { CasePostRequest } from '@kbn/cases-plugin/common/types/api'; +import type { + CasePostRequest, + PostFileAttachmentRequest, +} from '@kbn/cases-plugin/common/types/api'; import { FILE_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common/constants'; import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; @@ -64,6 +67,11 @@ export const postCommentUserReq: UserCommentAttachmentPayload = { owner: 'securitySolutionFixture', }; +export const postFileReq: PostFileAttachmentRequest = { + file: 'This is a file, a buffer will be created from this string.', + filename: 'foobar.txt', +}; + export const postCommentAlertReq: AlertAttachmentPayload = { alertId: 'test-id', index: 'test-index', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index fbfa4354683a5..d38af59f57d16 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -79,6 +79,9 @@ export default ({ getService }: FtrProviderContext): void => { describe('delete_cases', () => { afterEach(async () => { + await deleteAllFiles({ + supertest, + }); await deleteAllCaseItems(es); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/files/post_file.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/files/post_file.ts new file mode 100644 index 0000000000000..b5e27bd85e172 --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/files/post_file.ts @@ -0,0 +1,179 @@ +/* + * 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 expect from '@kbn/expect'; +import { ExternalReferenceAttachmentAttributes } from '@kbn/cases-plugin/common/types/domain'; +import { SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { defaultUser, postCaseReq, postFileReq } from '../../../../common/lib/mock'; +import { + createCase, + createFileAttachment, + removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, + deleteAllCaseItems, + superUserSpace1Auth, + deleteAllFiles, +} from '../../../../common/lib/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + const caseRequest = { ...postCaseReq, owner: SECURITY_SOLUTION_OWNER }; + + describe('post_file', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + describe('happy path', () => { + afterEach(async () => { + await deleteAllFiles({ + supertest, + auth: authSpace1, + }); + }); + + it('should post a file in space1', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + const patchedCase = await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: postFileReq, + auth: authSpace1, + }); + + const attachedFileComment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] + ) as ExternalReferenceAttachmentAttributes; + // @ts-ignore + const fileMetadata = attachedFileComment.externalReferenceMetadata?.files[0]; + + expect(attachedFileComment.owner).to.be('securitySolution'); + expect(attachedFileComment.externalReferenceId).to.be.ok(); // truthy + expect(attachedFileComment.externalReferenceAttachmentTypeId).to.be('.files'); + expect(attachedFileComment.externalReferenceStorage).to.eql({ + soType: 'file', + type: 'savedObject', + }); + expect(fileMetadata.name).to.be('foobar'); + expect(fileMetadata.mimeType).to.be('text/plain'); + expect(fileMetadata.extension).to.be('txt'); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + }); + + describe('unhappy path', () => { + it('should return a 400 when posting a file without a filename', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: { ...postFileReq, filename: '' }, + auth: authSpace1, + expectedHttpCode: 400, + }); + }); + + it('should return a 400 when posting a file with a filename that is too long', async () => { + const longFilename = Array(161).fill('a').toString(); + + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: { ...postFileReq, filename: longFilename }, + auth: authSpace1, + expectedHttpCode: 400, + }); + }); + + it('should return a 400 when posting a file with an invalid mime type', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + await createFileAttachment({ + supertest, + caseId: postedCase.id, + params: { ...postFileReq, filename: 'foobar.124zas' }, + auth: authSpace1, + expectedHttpCode: 400, + }); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should not post a file when the user does not have permissions for that owner', async () => { + const postedCase = await createCase(supertest, caseRequest, 200, authSpace1); + + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should not post a file`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + caseRequest, + 200, + superUserSpace1Auth + ); + + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not post a file in a space the user does not have permissions for', async () => { + const postedCase = await createCase(supertestWithoutAuth, caseRequest, 200, { + user: superUser, + space: 'space2', + }); + + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index f9360e473080d..6378ce936e9ea 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -21,6 +21,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./comments/get_all_comments')); loadTestFile(require.resolve('./comments/patch_comment')); loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./files/post_file')); loadTestFile(require.resolve('./alerts/get_cases')); loadTestFile(require.resolve('./alerts/get_alerts_attached_to_case')); loadTestFile(require.resolve('./cases/delete_cases')); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/files/post_file.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/files/post_file.ts new file mode 100644 index 0000000000000..8e624620b0d4e --- /dev/null +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/files/post_file.ts @@ -0,0 +1,86 @@ +/* + * 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 expect from '@kbn/expect'; +import { ExternalReferenceAttachmentAttributes } from '@kbn/cases-plugin/common/types/domain'; +import { SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { nullUser, postCaseReq, postFileReq } from '../../../../common/lib/mock'; +import { + createCase, + createFileAttachment, + removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, + deleteAllCaseItems, + deleteAllFiles, +} from '../../../../common/lib/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + // const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('post_file', () => { + afterEach(async () => { + await deleteAllFiles({ + supertest: supertestWithoutAuth, + auth: authSpace1, + }); + await deleteAllCaseItems(es); + }); + + it('should post a file in space1', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + { ...postCaseReq, owner: SECURITY_SOLUTION_OWNER }, + 200, + authSpace1 + ); + const patchedCase = await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: authSpace1, + }); + + const attachedFileComment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] + ) as ExternalReferenceAttachmentAttributes; + // @ts-ignore + const fileMetadata = attachedFileComment.externalReferenceMetadata?.files[0]; + + expect(attachedFileComment.owner).to.be('securitySolution'); + expect(attachedFileComment.externalReferenceId).to.be.ok(); // truthy + expect(attachedFileComment.externalReferenceAttachmentTypeId).to.be('.files'); + expect(attachedFileComment.externalReferenceStorage).to.eql({ + soType: 'file', + type: 'savedObject', + }); + expect(fileMetadata.name).to.be('foobar'); + expect(fileMetadata.mimeType).to.be('text/plain'); + expect(fileMetadata.extension).to.be('txt'); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(nullUser); + }); + + it('should not post a file on a case in a different space', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); + await createFileAttachment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postFileReq, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts index 81da0454f48a4..c8de077b16bca 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts @@ -17,6 +17,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./comments/get_all_comments')); loadTestFile(require.resolve('./comments/patch_comment')); loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./files/post_file')); loadTestFile(require.resolve('./cases/delete_cases')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./cases/get_case')); From 8cf1aacbc6d1959b911ab0f8184946ff540a5c67 Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 14 Nov 2024 17:07:51 +0000 Subject: [PATCH 08/16] [Ownership] Assign test files to Shared UX Team (#199779) ## Summary Assign test files to Shared UX Team Contributes to: https://github.com/elastic/kibana/issues/192979 --------- Co-authored-by: Robert Oskamp Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ff5ec12fef6a5..12c35f6251f6b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2115,19 +2115,44 @@ x-pack/test/profiling_api_integration @elastic/obs-ux-infra_services-team x-pack/plugins/observability_solution/observability_shared/public/components/profiling @elastic/obs-ux-infra_services-team # Shared UX -/test/api_integration/apis/guided_onboarding @elastic/appex-sharedux +/test/functional/page_objects/files_management.ts @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/200017#discussion_r1840477291 +/test/accessibility/apps/home.ts @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/199771/files#r1840077237 +/test/api_integration/apis/home/*.ts @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/199771/files#r1840077065 +/test/functional/apps/home @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/199771/files#r1840075278 +/x-pack/test/plugin_functional/plugins/global_search_test @elastic/appex-sharedux +/test/functional/services/saved_objects_finder.ts @elastic/appex-sharedux +/test/functional/apps/kibana_overview @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/config.ts @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/ftr_provider_context.ts @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/services.ts @elastic/appex-sharedux +/x-pack/test/functional_solution_sidenav/index.ts @elastic/appex-sharedux +/x-pack/test/functional_cloud @elastic/appex-sharedux +/x-pack/test/functional/services/user_menu.js @elastic/appex-sharedux # Assigned per https://github.com/elastic/kibana/pull/179270 +/x-pack/test/functional/page_objects/reporting_page.ts @elastic/appex-sharedux +/x-pack/test/accessibility/apps/**/config.ts @elastic/appex-sharedux +/x-pack/test/accessibility/apps/**/index.ts @elastic/appex-sharedux +/x-pack/test/upgrade/apps/reporting @elastic/appex-sharedux +/x-pack/test/functional/apps/advanced_settings @elastic/appex-sharedux +/test/functional/services/monaco_editor.ts @elastic/appex-sharedux +/x-pack/test/functional/fixtures/kbn_archiver/global_search @elastic/appex-sharedux +/x-pack/test/plugin_functional/test_suites/global_search @elastic/appex-sharedux /test/plugin_functional/test_suites/shared_ux @elastic/appex-sharedux -/x-pack/test/functional/apps/advanced_settings @elastic/appex-sharedux @elastic/kibana-management -/test/examples/content_management @elastic/appex-sharedux /test/plugin_functional/plugins/kbn_sample_panel_action @elastic/appex-sharedux -/test/functional/apps/kibana_overview @elastic/appex-sharedux /test/plugin_functional/plugins/eui_provider_dev_warning @elastic/appex-sharedux +/test/functional/page_objects/settings_page.ts @elastic/appex-sharedux +/test/functional/apps/sharing/*.ts @elastic/appex-sharedux +/test/functional/apps/kibana_overviews @elastic/appex-sharedux +/test/examples/ui_actions/*.ts @elastic/appex-sharedux +/test/examples/state_sync/*.ts @elastic/appex-sharedux +/test/examples/error_boundary/index.ts @elastic/appex-sharedux +/test/examples/content_management/*.ts @elastic/appex-sharedux +/test/examples/bfetch_explorer/*.ts @elastic/appex-sharedux +/test/api_integration/apis/guided_onboarding @elastic/appex-sharedux /x-pack/test/banners_functional @elastic/appex-sharedux /x-pack/test/custom_branding @elastic/appex-sharedux -/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @elastic/appex-sharedux -/x-pack/test/accessibility/apps/group3/tags.ts @elastic/appex-sharedux /x-pack/test/api_integration/apis/content_management @elastic/appex-sharedux -/x-pack/test/functional_solution_sidenav @elastic/appex-sharedux +/x-pack/test/accessibility/apps/group3/tags.ts @elastic/appex-sharedux +/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @elastic/appex-sharedux /x-pack/test_serverless/functional/test_suites/common/spaces/spaces_selection.ts @elastic/appex-sharedux /x-pack/test_serverless/functional/test_suites/common/spaces/index.ts @elastic/appex-sharedux packages/react @elastic/appex-sharedux From c30c28f317fafb6997bdbbea22f66d85c58b4865 Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 14 Nov 2024 17:10:51 +0000 Subject: [PATCH 09/16] [Ownership] Assign test files to management team (#200017) ## Summary Assign test files to management team Contributes to: #192979 --- .github/CODEOWNERS | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 12c35f6251f6b..8e8761ce39dcb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1667,18 +1667,51 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints /x-pack/test/functional_search/ @elastic/search-kibana # Management Experience - Deployment Management -/x-pack/test/api_integration/services/index_management.ts @elastic/kibana-management -/x-pack/test/functional/apps/license_management @elastic/kibana-management +/x-pack/test/functional/services/ace_editor.js @elastic/kibana-management +/x-pack/test/functional/page_objects/remote_clusters_page.ts @elastic/kibana-management +/x-pack/test/stack_functional_integration/apps/ccs @elastic/kibana-management +/x-pack/test/functional/services/data_stream.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/watcher_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/snapshot_restore_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/rollup_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/ingest_pipelines_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/grok_debugger_page.ts @elastic/kibana-management +/x-pack/test/functional/page_objects/cross_cluster_replication_page.ts @elastic/kibana-management +/x-pack/test/functional/fixtures/ingest_pipeline_example_mapping.csv @elastic/kibana-management +/x-pack/test/functional/apps/snapshot_restore @elastic/kibana-management /x-pack/test/functional/apps/painless_lab @elastic/kibana-management +/x-pack/test_serverless/functional/test_suites/common/spaces/spaces_management.ts @elastic/kibana-management +/x-pack/test/stack_functional_integration/apps/management @elastic/kibana-management +/x-pack/test/functional/page_objects/*_management_page.ts @elastic/kibana-management +/test/functional/services/saved_query_management_component.ts @elastic/kibana-management +/test/functional/services/management @elastic/kibana-management +/x-pack/test/functional/apps/cross_cluster_replication @elastic/kibana-management +/x-pack/test/functional/apps/ingest_pipelines @elastic/kibana-management +/x-pack/test/functional/apps/home @elastic/kibana-management +/x-pack/test/functional/apps/license_management @elastic/kibana-management /x-pack/test/functional/apps/management @elastic/kibana-management +/x-pack/test/functional/apps/remote_clusters @elastic/kibana-management +/x-pack/test/functional/apps/upgrade_assistant @elastic/kibana-management +/x-pack/test/functional/apps/dev_tools @elastic/kibana-management +/test/plugin_functional/test_suites/management @elastic/kibana-management +/x-pack/test/upgrade_assistant_integration @elastic/kibana-management +/test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management +/test/functional/page_objects/management/*.ts @elastic/kibana-management +/test/functional/page_objects/embedded_console.ts @elastic/kibana-management +/test/functional/page_objects/console_page.ts @elastic/kibana-management +/test/functional/firefox/console.config.ts @elastic/kibana-management +/test/functional/apps/management @elastic/kibana-management +/test/functional/apps/saved_objects_management @elastic/kibana-management +/test/functional/apps/console/*.ts @elastic/kibana-management +/test/api_integration/apis/console/*.ts @elastic/kibana-management +/test/accessibility/apps/management.ts @elastic/kibana-management +/test/accessibility/apps/console.ts @elastic/kibana-management /x-pack/test/api_integration/services/index_management.ts @elastic/kibana-management /x-pack/test/functional/services/grok_debugger.js @elastic/kibana-management -/x-pack/test/functional/apps/cross_cluster_replication @elastic/kibana-management /x-pack/test/functional/apps/grok_debugger @elastic/kibana-management /x-pack/test/functional/apps/index_lifecycle_management @elastic/kibana-management /x-pack/test/functional/apps/index_management @elastic/kibana-management -/x-pack/test/functional/apps/ingest_pipelines @elastic/kibana-management -/x-pack/test/functional/apps/upgrade_assistant @elastic/kibana-management /x-pack/test/api_integration/services/ingest_pipelines @elastic/kibana-management /x-pack/test/functional/apps/watcher @elastic/kibana-management /x-pack/test/api_integration/apis/watcher @elastic/kibana-management From 1ad670439f26ef0265466c02264e76c89a22f188 Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 14 Nov 2024 17:20:03 +0000 Subject: [PATCH 10/16] [Ownership] Assign test files to security-solution team (#200138) ## Summary Assign test files to security-solution team Contributes to: #192979 Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8e8761ce39dcb..77683d6a94916 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1752,9 +1752,12 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/kibana-management # Security Solution +/x-pack/test/functional/es_archives/rule_exceptions @elastic/security-solution # Assigned per https://github.com/elastic/kibana/pull/199795/files/ae80bb252bc71f787c122849fcb9b01e386fc5e9#r1840233040 +/x-pack/test/functional_solution_sidenav/tests/security_sidenav.ts @elastic/security-solution +/x-pack/test/common/utils/security_solution/index.ts @elastic/security-solution +/x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution /x-pack/test/common/services/security_solution @elastic/security-solution /x-pack/test/api_integration/services/security_solution_*.gen.ts @elastic/security-solution -/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts @elastic/security-solution /x-pack/test/accessibility/apps/group3/security_solution.ts @elastic/security-solution /x-pack/test_serverless/functional/test_suites/security/config.ts @elastic/security-solution @elastic/appex-qa /x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts @elastic/security-solution From 8dafb11009995f637c71b5689cf6a803d2c9a6fd Mon Sep 17 00:00:00 2001 From: Sandra G Date: Thu, 14 Nov 2024 12:21:30 -0500 Subject: [PATCH 11/16] [Obs AI Assistant] fix spelling error (#199867) ## Summary Fixes spelling error from "degradated" to "degraded". --- .../src/chat/simulated_function_calling_callout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx index 26eb589b25dfc..b570a0cae11f0 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx @@ -19,7 +19,7 @@ export function SimulatedFunctionCallingCallout() { {i18n.translate('xpack.aiAssistant.simulatedFunctionCallingCalloutLabel', { defaultMessage: - 'Simulated function calling is enabled. You might see degradated performance.', + 'Simulated function calling is enabled. You might see degraded performance.', })} From 4e852ea041b63e3e3ad918ceee1bc3861dd1e519 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Thu, 14 Nov 2024 17:26:24 +0000 Subject: [PATCH 12/16] [Stack Monitoring / Logs] Fix Stack Monitoring logs links (#200043) ## Summary Fixes https://github.com/elastic/kibana/issues/199902#issuecomment-2474040264. This was missed in https://github.com/elastic/kibana/pull/190835 due to Stack Monitoring's lack of type checking in certain files. `infra` no longer exposes `logsLocators`. This uses `getLogsLocatorsFromUrlService`, technically we could go to Discover now knowing that Logs Explorer will be deprecated in the future. But it will make more sense to convert `getLogsLocatorsFromUrlService` over to using the Discover locators in one when that happens. This puts us on the same page as https://github.com/elastic/kibana/pull/190835. This link should now work, and have the correct filters applied. ![Screenshot 2024-11-13 at 16 09 15](https://github.com/user-attachments/assets/e1f8fd18-ac73-4179-af4c-1727a2afeeec) --------- Co-authored-by: mohamedhamed-ahmed --- x-pack/plugins/monitoring/kibana.jsonc | 6 ++-- .../monitoring/public/components/logs/logs.js | 25 ++++++++++---- .../public/components/logs/logs.test.js | 33 +++++++++++++++---- x-pack/plugins/monitoring/public/plugin.ts | 2 ++ x-pack/plugins/monitoring/public/types.ts | 2 ++ x-pack/plugins/monitoring/tsconfig.json | 1 + 6 files changed, 53 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/monitoring/kibana.jsonc b/x-pack/plugins/monitoring/kibana.jsonc index 51272f995b012..5b9b2853357b9 100644 --- a/x-pack/plugins/monitoring/kibana.jsonc +++ b/x-pack/plugins/monitoring/kibana.jsonc @@ -21,7 +21,8 @@ "observability", "observabilityShared", "dataViews", - "unifiedSearch" + "unifiedSearch", + "share" ], "optionalPlugins": [ "infra", @@ -39,7 +40,8 @@ "requiredBundles": [ "kibanaUtils", "alerting", - "kibanaReact" + "kibanaReact", + "logsShared" ] } } \ No newline at end of file diff --git a/x-pack/plugins/monitoring/public/components/logs/logs.js b/x-pack/plugins/monitoring/public/components/logs/logs.js index 3678c4ae9e27f..3da4bc9dc13ff 100644 --- a/x-pack/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.js @@ -10,11 +10,12 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { upperFirst } from 'lodash'; import { Legacy } from '../../legacy_shims'; import { EuiBasicTable, EuiTitle, EuiSpacer, EuiText, EuiCallOut, EuiLink } from '@elastic/eui'; -import { INFRA_SOURCE_ID } from '../../../common/constants'; import { formatDateTimeLocal } from '../../../common/formatting'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { Reason } from './reason'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; const getFormattedDateTimeLocal = (timestamp) => { const timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); @@ -110,7 +111,7 @@ const clusterColumns = [ }, ]; -function getLogsUiLink(clusterUuid, nodeId, indexUuid) { +function getLogsUiLink(clusterUuid, nodeId, indexUuid, sharePlugin) { const params = []; if (clusterUuid) { params.push(`elasticsearch.cluster.uuid:${clusterUuid}`); @@ -122,15 +123,23 @@ function getLogsUiLink(clusterUuid, nodeId, indexUuid) { params.push(`elasticsearch.index.name:${indexUuid}`); } - const base = Legacy.shims.infra.locators.logsLocator.getRedirectUrl({ - logView: { logViewId: INFRA_SOURCE_ID, type: 'log-view-reference' }, - ...(params.length ? { filter: params.join(' and ') } : {}), + const filter = params.join(' and '); + const { logsLocator } = getLogsLocatorsFromUrlService(sharePlugin.url); + + const base = logsLocator.getRedirectUrl({ + filter, }); return base; } -export class Logs extends PureComponent { +export const Logs = (props) => { + const { + services: { share }, + } = useKibana(); + return ; +}; +export class LogsContent extends PureComponent { renderLogs() { const { logs: { enabled, logs }, @@ -165,7 +174,9 @@ export class Logs extends PureComponent { nodeId, clusterUuid, indexUuid, + sharePlugin, } = this.props; + if (!enabled || !show) { return null; } @@ -184,7 +195,7 @@ export class Logs extends PureComponent { defaultMessage="Visit {link} to dive deeper." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.listing.calloutLinkText', { defaultMessage: 'Logs', })} diff --git a/x-pack/plugins/monitoring/public/components/logs/logs.test.js b/x-pack/plugins/monitoring/public/components/logs/logs.test.js index 23292ff742fcd..1117a484a294c 100644 --- a/x-pack/plugins/monitoring/public/components/logs/logs.test.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.test.js @@ -7,7 +7,18 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Logs } from './logs'; +import { LogsContent } from './logs'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; + +const sharePlugin = sharePluginMock.createStartContract(); + +jest.mock('@kbn/logs-shared-plugin/common', () => { + return { + getLogsLocatorsFromUrlService: jest.fn().mockReturnValue({ + logsLocator: { getRedirectUrl: jest.fn(() => '') }, + }), + }; +}); jest.mock('../../legacy_shims', () => ({ Legacy: { @@ -123,32 +134,40 @@ const logs = { describe('Logs', () => { it('should render normally', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); it('should render fewer columns for node or index view', () => { - const component = shallow(); + const component = shallow(); expect(component.find('EuiBasicTable').prop('columns')).toMatchSnapshot(); }); it('should render a link to filter by cluster uuid', () => { - const component = shallow(); + const component = shallow( + + ); expect(component.find('EuiCallOut')).toMatchSnapshot(); }); it('should render a link to filter by cluster uuid and node uuid', () => { - const component = shallow(); + const component = shallow( + + ); expect(component.find('EuiCallOut')).toMatchSnapshot(); }); it('should render a link to filter by cluster uuid and index uuid', () => { - const component = shallow(); + const component = shallow( + + ); expect(component.find('EuiCallOut')).toMatchSnapshot(); }); it('should render a reason if the logs are disabled', () => { - const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index c6cfbdc48094c..e29faa9ca5d86 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -101,6 +101,7 @@ export class MonitoringPlugin element: params.element, core: coreStart, data: pluginsStart.data, + share: pluginsStart.share, isCloud: Boolean(plugins.cloud?.isCloudEnabled), pluginInitializerContext: this.initializerContext, externalConfig, @@ -124,6 +125,7 @@ export class MonitoringPlugin appMountParameters: deps.appMountParameters, dataViews: deps.dataViews, infra: deps.infra, + share: deps.share, }); const config = Object.fromEntries(externalConfig); diff --git a/x-pack/plugins/monitoring/public/types.ts b/x-pack/plugins/monitoring/public/types.ts index 5e6590968d232..cd5314521f947 100644 --- a/x-pack/plugins/monitoring/public/types.ts +++ b/x-pack/plugins/monitoring/public/types.ts @@ -16,6 +16,7 @@ import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { FleetStart } from '@kbn/fleet-plugin/public'; import type { InfraClientStartExports } from '@kbn/infra-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; export interface MonitoringStartPluginDependencies { navigation: NavigationStart; @@ -26,6 +27,7 @@ export interface MonitoringStartPluginDependencies { dashboard?: DashboardStart; fleet?: FleetStart; infra?: InfraClientStartExports; + share: SharePluginStart; } interface LegacyStartDependencies { diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index 3b78104e65b8c..48b538a00c1eb 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -48,6 +48,7 @@ "@kbn/flot-charts", "@kbn/ui-theme", "@kbn/core-elasticsearch-server", + "@kbn/share-plugin", ], "exclude": [ "target/**/*", From 190430b0b2011235963af069a0196cfafb7a5cd5 Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:28:50 +0100 Subject: [PATCH 13/16] [Spaces] Open 'manage roles' link for spaces assign role flyout in new tab (#199506) ## Summary Closes https://github.com/elastic/kibana-team/issues/1281 Modify the "manage role" link in the assign roles to space tab, so it opens the roles screen in a new tab, with an improvement so that on transitioning back to the assign roles space tab the user is presented with an updated list of roles created in the page that was created opened, if any. Caveat: This approach will continually make calls to refresh the role list on every page visibility event that matches the conditions provided until the flyout gets closed. ##### Visuals https://github.com/user-attachments/assets/64cb296d-246d-4033-a655-7b10d0dafab1 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../space_assign_role_privilege_form.test.tsx | 28 +++++++++++- .../space_assign_role_privilege_form.tsx | 43 ++++++++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index 1c97b9c4d2bc6..d35bdbee4b8bb 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -6,7 +6,7 @@ */ import { EuiThemeProvider } from '@elastic/eui'; -import { render, screen, waitFor, within } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import crypto from 'crypto'; import React from 'react'; @@ -142,6 +142,15 @@ describe('PrivilegesRolesForm', () => { jest.clearAllMocks(); }); + it("would open the 'manage roles' link in a new tab", () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + renderPrivilegeRolesForm(); + + expect(screen.getByText('Manage roles')).toHaveAttribute('target', '_blank'); + }); + it('does not display the privilege selection buttons or customization form when no role is selected', async () => { getRolesSpy.mockResolvedValue([]); getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); @@ -170,6 +179,23 @@ describe('PrivilegesRolesForm', () => { expect(screen.getByTestId('space-assign-role-create-roles-privilege-button')).toBeDisabled(); }); + it('makes a request to refetch available roles if page transitions back from a user interaction page visibility change', () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + renderPrivilegeRolesForm(); + + expect(getRolesSpy).toHaveBeenCalledTimes(1); + + // trigger click on manage roles link, which is perquisite for page visibility handler to trigger role refetch + fireEvent.click(screen.getByText(/manage roles/i)); + + // trigger page visibility change + fireEvent(document, new Event('visibilitychange')); + + expect(getRolesSpy).toHaveBeenCalledTimes(2); + }); + it('renders with the assign roles button disabled when no base privileges or feature privileges are selected', async () => { getRolesSpy.mockResolvedValue([]); getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index e0f3e8f3714c6..74f2b2fde4667 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -23,6 +23,7 @@ import { EuiSpacer, EuiText, EuiTitle, + useGeneratedHtmlId, } from '@elastic/eui'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import type { FC } from 'react'; @@ -81,10 +82,13 @@ export const PrivilegesRolesForm: FC = (props) => { const [selectedRoles, setSelectedRoles] = useState>( createRolesComboBoxOptions(defaultSelected) ); + const manageRoleLinkId = useGeneratedHtmlId(); const isEditOperation = useRef(Boolean(defaultSelected.length)); - useEffect(() => { - async function fetchRequiredData(spaceId: string) { + const userInvokedPageVisibilityChange = useRef(null); + + const fetchRequiredData = useCallback( + async (spaceId: string) => { setFetchingDataDeps(true); const [systemRoles, _kibanaPrivileges] = await invokeClient((clients) => @@ -109,10 +113,28 @@ export const PrivilegesRolesForm: FC = (props) => { ); setKibanaPrivileges(_kibanaPrivileges); - } + }, + [invokeClient] + ); + useEffect(() => { fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); - }, [invokeClient, space.id]); + }, [fetchRequiredData, invokeClient, space.id]); + + useEffect(() => { + async function visibilityChangeHandler() { + // page just transitioned back to visible state from hidden state caused by user interaction + if (userInvokedPageVisibilityChange.current && !document.hidden) { + await fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); + } + } + + document.addEventListener('visibilitychange', visibilityChangeHandler); + + return () => { + document.removeEventListener('visibilitychange', visibilityChangeHandler); + }; + }, [fetchRequiredData, invokeClient, space.id]); const selectedRolesCombinedPrivileges = useMemo(() => { const combinedPrivilege = new Set( @@ -345,12 +367,23 @@ export const PrivilegesRolesForm: FC = (props) => { {!isEditOperation.current && ( ) => { + // leverage event propagation, check if manage role link element was clicked + if ((event.target as HTMLFieldSetElement).id === manageRoleLinkId) { + userInvokedPageVisibilityChange.current = true; + } + }} label={i18n.translate( 'xpack.spaces.management.spaceDetails.roles.selectRolesFormRowLabel', { defaultMessage: 'Select roles' } )} labelAppend={ - + {i18n.translate( 'xpack.spaces.management.spaceDetails.roles.selectRolesFormRowLabelAnchor', { defaultMessage: 'Manage roles' } From f552a06c674ab4d4acf00aed6e1bf74f16c92a81 Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 14 Nov 2024 17:39:48 +0000 Subject: [PATCH 14/16] [Ownership] Assign test files to fleet team (#200180) ## Summary Assign test files to fleet team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 77683d6a94916..2b06c284b6a80 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1248,9 +1248,10 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/accessibility/apps/group3/stack_monitoring.ts @elastic/stack-monitoring # Fleet -/x-pack/test_serverless/api_integration/test_suites/security/fleet @elastic/fleet -/x-pack/test/fleet_packages @elastic/fleet +/x-pack/test/api_integration/services/fleet_and_agents.ts @elastic/fleet /x-pack/test/fleet_api_integration @elastic/fleet +/x-pack/test/fleet_packages @elastic/fleet +/test/api_integration/apis/custom_integration/*.ts @elastic/fleet /x-pack/test/fleet_cypress @elastic/fleet /x-pack/test/fleet_functional @elastic/fleet /src/dev/build/tasks/bundle_fleet_packages.ts @elastic/fleet @elastic/kibana-operations From ed002eca649a50b6cdf5446d2272d152dfb36e15 Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 14 Nov 2024 17:42:18 +0000 Subject: [PATCH 15/16] [Ownership] Assign test files to visualizations team (#200144) ## Summary Assign test files to visualizations team Contributes to: #192979 --------- Co-authored-by: Stratoula Kalafateli --- .github/CODEOWNERS | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b06c284b6a80..99bd6d6b0757d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1100,6 +1100,32 @@ src/plugins/discover/public/context_awareness/profile_providers/security @elasti /x-pack/test/screenshot_creation @elastic/platform-docs # Visualizations +/test/functional/apps/getting_started/*.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/199767#discussion_r1840485031 +/x-pack/test/upgrade/apps/graph @elastic/kibana-visualizations +/x-pack/test/functional/page_objects/log_wrapper.ts @elastic/kibana-visualizations # Assigned per https://github.com/elastic/kibana/pull/36437 +/x-pack/test/functional/page_objects/graph_page.ts @elastic/kibana-visualizations +/x-pack/test/functional/apps/managed_content @elastic/kibana-visualizations +/test/functional/services/visualizations @elastic/kibana-visualizations +/test/functional/services/renderable.ts @elastic/kibana-visualizations +/test/functional/page_objects/vega_chart_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/visual_builder_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/visualize_*.ts @elastic/kibana-visualizations +/test/functional/page_objects/timelion_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/time_to_visualize_page.ts @elastic/kibana-visualizations +/test/functional/page_objects/tag_cloud_page.ts @elastic/kibana-visualizations +/x-pack/test/functional/page_objects/lens_page.ts @elastic/kibana-visualizations +/x-pack/test/functional/es_archives/lens @elastic/kibana-visualizations +/x-pack/test/examples/embedded_lens @elastic/kibana-visualizations +/x-pack/test/api_integration/fixtures/kbn_archiver/lens/constant_keyword.json @elastic/kibana-visualizations +/test/plugin_functional/test_suites/custom_visualizations @elastic/kibana-visualizations +/test/plugin_functional/plugins/kbn_tp_custom_visualizations @elastic/kibana-visualizations +/x-pack/test/functional/fixtures/kbn_archiver/visualize @elastic/kibana-visualizations +/x-pack/test/functional/fixtures/kbn_archiver/lens @elastic/kibana-visualizations +/test/functional/page_objects/legacy/data_table_vis.ts @elastic/kibana-visualizations +/test/functional/page_objects/annotation_library_editor_page.ts @elastic/kibana-visualizations # Assigned per git blame +/test/functional/firefox/visualize.config.ts @elastic/kibana-visualizations +/test/examples/expressions_explorer/*.ts @elastic/kibana-visualizations +/test/accessibility/apps/visualize.ts @elastic/kibana-visualizations /x-pack/test/accessibility/apps/group3/graph.ts @elastic/kibana-visualizations /x-pack/test/accessibility/apps/group2/lens.ts @elastic/kibana-visualizations /x-pack/test/functional/apps/visualize @elastic/kibana-visualizations @@ -1113,6 +1139,10 @@ src/plugins/discover/public/context_awareness/profile_providers/security @elasti /x-pack/test_serverless/functional/fixtures/kbn_archiver/lens/ @elastic/kibana-visualizations packages/kbn-monaco/src/esql @elastic/kibana-esql +# ESQL +/test/api_integration/apis/esql/*.ts @elastic/kibana-esql +/test/functional/services/esql.ts @elastic/kibana-esql + # Global Experience ### Global Experience Reporting From 4d1cb0b586c3434d2c968c059d6e23d71207bfef Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:50:37 +0000 Subject: [PATCH 16/16] [obsUX][APM] Migrate APM metrics tests to agnostic deployment tests (#200175) ### Summary Closes https://github.com/elastic/kibana/issues/198976 Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate metrics test folder to Deployment-agnostic testing strategy. #### How to test Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` It's recommended to be run against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` Checks - [ ] (OPTIONAL, only if a test has been unskipped) Run flaky test suite - [x] local run for serverless - [x] local run for stateful - [x] MKI run for serverless --- .../apis/observability/apm/index.ts | 1 + .../apis/observability/apm/metrics/index.ts | 19 + .../apm}/metrics/memory/generate_data.ts | 0 .../metrics/memory/memory_metrics.spec.ts | 15 +- .../apm/metrics/metrics_charts.spec.ts | 461 +++++++++++++++++ .../apm}/metrics/serverless/generate_data.ts | 0 .../serverless_active_instances.spec.ts | 15 +- .../serverless_functions_overview.spec.ts | 16 +- .../serverless_metrics_charts.spec.ts | 331 +++++++++++++ .../serverless/serverless_summary.spec.ts | 110 +++++ .../es_archiver/metrics_8.0.0/mappings.json | 7 +- .../tests/metrics/metrics_charts.spec.ts | 462 ------------------ .../serverless_metrics_charts.spec.ts | 333 ------------- .../serverless/serverless_summary.spec.ts | 111 ----- 14 files changed, 948 insertions(+), 933 deletions(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/metrics/memory/generate_data.ts (100%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/metrics/memory/memory_metrics.spec.ts (85%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/metrics_charts.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/metrics/serverless/generate_data.ts (100%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/metrics/serverless/serverless_active_instances.spec.ts (89%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/metrics/serverless/serverless_functions_overview.spec.ts (84%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_metrics_charts.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_summary.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/metrics/metrics_charts.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index d115e10f2373a..b46b2c3f10637 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -23,6 +23,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./correlations')); loadTestFile(require.resolve('./entities')); loadTestFile(require.resolve('./cold_start')); + loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./services')); loadTestFile(require.resolve('./historical_data')); loadTestFile(require.resolve('./observability_overview')); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/index.ts new file mode 100644 index 0000000000000..454e3b16e9aad --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/index.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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('metrics', () => { + loadTestFile(require.resolve('./metrics_charts.spec.ts')); + loadTestFile(require.resolve('./memory/memory_metrics.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_active_instances.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_functions_overview.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_metrics_charts.spec.ts')); + loadTestFile(require.resolve('./serverless/serverless_summary.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/generate_data.ts diff --git a/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/memory_metrics.spec.ts similarity index 85% rename from x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/memory_metrics.spec.ts index e0f8fc1cf28c2..0bcfff6395fef 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/memory/memory_metrics.spec.ts @@ -6,13 +6,13 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { config, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -33,9 +33,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - // FLAKY: https://github.com/elastic/kibana/issues/176990 - registry.when('Memory', { config: 'trial', archives: [] }, () => { + describe('Memory', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/metrics_charts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/metrics_charts.spec.ts new file mode 100644 index 0000000000000..f801113fdf823 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/metrics_charts.spec.ts @@ -0,0 +1,461 @@ +/* + * 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 expect from '@kbn/expect'; +import { first } from 'lodash'; +import { GenericMetricsChart } from '@kbn/apm-plugin/server/routes/metrics/fetch_and_transform_metrics'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; +import { SupertestReturnType } from '../../../../services/apm_api'; + +type ChartResponse = SupertestReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); + + describe('Metrics charts when data is loaded', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['metrics_8.0.0']); + }); + + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['metrics_8.0.0']); + }); + + describe('for opbeans-node', () => { + describe('returns metrics data', () => { + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start: '2020-09-08T14:50:00.000Z', + end: '2020-09-08T14:55:00.000Z', + agentName: 'nodejs', + environment: 'ENVIRONMENT_ALL', + kuery: ``, + }, + }, + }); + }); + + it('contains CPU usage and System memory usage chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.714, + 0.3877, + 0.75, + 0.2543, + ] + `); + }); + }); + + describe("System memory usage (using 'system.memory' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.722093920925555, + 0.718173546796348, + ] + `); + }); + }); + }); + }); + + describe('for opbeans-java', () => { + describe('returns metrics data', () => { + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start: '2020-09-08T14:55:30.000Z', + end: '2020-09-08T15:00:00.000Z', + agentName: 'java', + environment: 'ENVIRONMENT_ALL', + kuery: ``, + }, + }, + }); + }); + + it('has correct chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + "Heap Memory", + "Non-Heap Memory", + "Thread Count", + "Garbage collection per minute", + "Garbage collection time spent per minute", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.203, + 0.178777777777778, + 0.01, + 0.009, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.193, + 0.193, + 0.009, + 0.009, + ] + `); + }); + }); + + describe("System memory usage (using 'system.process.cgroup' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.707924703557837, + 0.705395980841182, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.707924703557837, + 0.707924703557837, + ] + `); + }); + }); + + describe('Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + "Avg. limit", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 222501617.777778, + 374341632, + 1560281088, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 211472896, + 374341632, + 1560281088, + ] + `); + }); + }); + + describe('Non-Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'non_heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 138573397.333333, + 147677639.111111, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 138162752, + 147386368, + ] + `); + }); + }); + + describe('Thread Count', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'thread_count_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. count", + "Max count", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 44.4444444444444, + 45, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 44, + 44, + ] + `); + }); + }); + + describe('Garbage collection per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_rate_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 3, + ] + `); + }); + }); + + describe('Garbage collection time spent per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_time_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 37500, + ] + `); + }); + }); + }); + + // 9223372036854771712 = memory limit for a c-group when no memory limit is specified + it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => { + const chartsResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start: '2020-09-08T15:00:30.000Z', + end: '2020-09-08T15:05:00.000Z', + agentName: 'java', + environment: 'ENVIRONMENT_ALL', + kuery: ``, + }, + }, + }); + + const systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.114523896426499, + 0.114002376090415, + ] + `); + + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.11383724014064, + 0.11383724014064, + ] + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/generate_data.ts diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_active_instances.spec.ts similarity index 89% rename from x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_active_instances.spec.ts index 1b15e03c91987..b490482b4dd52 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_active_instances.spec.ts @@ -6,15 +6,15 @@ */ import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import expect from '@kbn/expect'; import { sumBy } from 'lodash'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { config, expectedValues, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -36,8 +36,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - // FLAKY: https://github.com/elastic/kibana/issues/177639 - registry.when('Serverless active instances', { config: 'basic', archives: [] }, () => { + describe('Serverless active instances', () => { const { memoryTotal, billedDurationMs, @@ -47,8 +46,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { } = config; const { expectedMemoryUsed } = expectedValues; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_functions_overview.spec.ts similarity index 84% rename from x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_functions_overview.spec.ts index 94792228a2859..3acd0921d2602 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_functions_overview.spec.ts @@ -7,13 +7,13 @@ import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { config, expectedValues, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -34,8 +34,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - // FLAKY: https://github.com/elastic/kibana/issues/177641 - registry.when('Serverless functions overview', { config: 'basic', archives: [] }, () => { + describe('Serverless functions overview', () => { const { memoryTotal, billedDurationMs, @@ -45,7 +44,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { } = config; const { expectedMemoryUsed } = expectedValues; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_metrics_charts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_metrics_charts.spec.ts new file mode 100644 index 0000000000000..7f1e3c2a05004 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_metrics_charts.spec.ts @@ -0,0 +1,331 @@ +/* + * 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 expect from '@kbn/expect'; +import { meanBy, sumBy } from 'lodash'; +import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { generateData, config } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; + +function isNotNullOrZeroCoordinate(coordinate: Coordinate) { + return coordinate.y !== null && coordinate.y !== 0; +} + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const numberOfTransactionsCreated = 15; + + async function callApi(serviceName: string, serverlessId?: string) { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metrics/serverless/charts', + params: { + path: { serviceName }, + query: { + environment: 'test', + kuery: '', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + ...(serverlessId ? { serverlessId } : {}), + }, + }, + }); + } + + describe('Serverless metrics charts', () => { + describe('when data is not loaded', () => { + let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessMetrics = response.body; + }); + + it('returns empty', () => { + serverlessMetrics.charts.forEach((chart) => { + expect(chart.series).to.be.empty(); + }); + }); + }); + + describe('when data is loaded', () => { + const { + memoryTotal, + memoryFree, + billedDurationMs, + coldStartDurationPython, + transactionDuration, + pythonServerlessFunctionNames, + serverlessId, + } = config; + + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('Python service', () => { + let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessMetrics = response.body; + }); + + it('returns all metrics chart', () => { + expect(serverlessMetrics.charts.length).to.be.greaterThan(0); + expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ + 'Cold start duration', + 'Cold starts', + 'Compute usage', + 'Lambda Duration', + 'System memory usage', + ]); + }); + + describe('Avg. Duration', () => { + const transactionDurationInMicroSeconds = transactionDuration * 1000; + [ + { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, + { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const avgDurationMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'avg_duration'; + }); + const series = avgDurationMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; + + describe('Cold start duration', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_duration'; + }); + }); + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); + }); + + it('returns correct mean value', () => { + const meanValue = meanBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(meanValue).to.equal(coldStartDurationPython * 1000); + }); + }); + + describe('Cold start count', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_count'; + }); + }); + + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal( + numberOfTransactionsCreated * pythonServerlessFunctionNames.length + ); + }); + + it('returns correct sum value', () => { + const sumValue = sumBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(sumValue).to.equal( + numberOfTransactionsCreated * pythonServerlessFunctionNames.length + ); + }); + }); + + describe('memory usage', () => { + const expectedFreeMemory = 1 - memoryFree / memoryTotal; + [ + { title: 'Max', expectedValue: expectedFreeMemory }, + { title: 'Average', expectedValue: expectedFreeMemory }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'memory_usage_chart'; + }); + const series = memoryUsageMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + describe('Compute usage', () => { + const GBSeconds = 1024 * 1024 * 1024 * 1000; + let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; + before(() => { + computeUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'compute_usage'; + }); + }); + it('returns correct overall value', () => { + const expectedValue = + ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated * 2; + expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); + }); + + it('returns correct mean value', () => { + const expectedValue = ((memoryTotal * billedDurationMs) / GBSeconds) * 2; + const meanValue = meanBy( + computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), + 'y' + ); + expect(meanValue).to.equal(expectedValue); + }); + }); + }); + + describe('detailed metrics', () => { + let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; + before(async () => { + const response = await callApi( + 'lambda-python', + `${serverlessId}${pythonServerlessFunctionNames[0]}` + ); + serverlessMetrics = response.body; + }); + + it('returns all metrics chart', () => { + expect(serverlessMetrics.charts.length).to.be.greaterThan(0); + expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ + 'Cold start duration', + 'Cold starts', + 'Compute usage', + 'Lambda Duration', + 'System memory usage', + ]); + }); + + describe('Avg. Duration', () => { + const transactionDurationInMicroSeconds = transactionDuration * 1000; + [ + { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, + { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const avgDurationMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'avg_duration'; + }); + const series = avgDurationMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; + + describe('Cold start duration', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_duration'; + }); + }); + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); + }); + + it('returns correct mean value', () => { + const meanValue = meanBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(meanValue).to.equal(coldStartDurationPython * 1000); + }); + }); + + describe('Cold start count', () => { + before(() => { + metricsChart = serverlessMetrics.charts.find((chart) => { + return chart.key === 'cold_start_count'; + }); + }); + + it('returns correct overall value', () => { + expect(metricsChart?.series[0].overallValue).to.equal(numberOfTransactionsCreated); + }); + + it('returns correct sum value', () => { + const sumValue = sumBy( + metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), + 'y' + ); + expect(sumValue).to.equal(numberOfTransactionsCreated); + }); + }); + + describe('memory usage', () => { + const expectedFreeMemory = 1 - memoryFree / memoryTotal; + [ + { title: 'Max', expectedValue: expectedFreeMemory }, + { title: 'Average', expectedValue: expectedFreeMemory }, + ].map(({ title, expectedValue }) => + it(`returns correct ${title} value`, () => { + const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'memory_usage_chart'; + }); + const series = memoryUsageMetric?.series.find((item) => item.title === title); + expect(series?.overallValue).to.eql(expectedValue); + const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); + expect(meanValue).to.eql(expectedValue); + }) + ); + }); + + describe('Compute usage', () => { + const GBSeconds = 1024 * 1024 * 1024 * 1000; + let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; + before(() => { + computeUsageMetric = serverlessMetrics.charts.find((chart) => { + return chart.key === 'compute_usage'; + }); + }); + it('returns correct overall value', () => { + const expectedValue = + ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated; + expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); + }); + + it('returns correct mean value', () => { + const expectedValue = (memoryTotal * billedDurationMs) / GBSeconds; + const meanValue = meanBy( + computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), + 'y' + ); + expect(meanValue).to.equal(expectedValue); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_summary.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_summary.spec.ts new file mode 100644 index 0000000000000..c291ffab47648 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/metrics/serverless/serverless_summary.spec.ts @@ -0,0 +1,110 @@ +/* + * 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import expect from '@kbn/expect'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { config, expectedValues, generateData } from './generate_data'; +import { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + async function callApi(serviceName: string, serverlessId?: string) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/metrics/serverless/summary`, + params: { + path: { serviceName }, + query: { + environment: 'test', + kuery: '', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + ...(serverlessId ? { serverlessId } : {}), + }, + }, + }); + } + + describe('Serverless overview', () => { + describe('when data is not loaded', () => { + let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessSummary = response.body; + }); + + it('returns empty', () => { + expect(serverlessSummary).to.be.empty(); + }); + }); + + describe('when data is loaded', () => { + const { billedDurationMs, pythonServerlessFunctionNames, faasDuration, serverlessId } = + config; + const { expectedMemoryUsedRate } = expectedValues; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('Python service', () => { + let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; + before(async () => { + const response = await callApi('lambda-python'); + serverlessSummary = response.body; + }); + + it('returns correct memory avg', () => { + expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); + }); + it('returns correct serverless function total', () => { + expect(serverlessSummary.serverlessFunctionsTotal).to.eql( + pythonServerlessFunctionNames.length + ); + }); + it('returns correct serverless duration avg', () => { + expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); + }); + it('returns correct billed duration avg', () => { + expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); + }); + }); + + describe('detailed metrics', () => { + let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; + before(async () => { + const response = await callApi( + 'lambda-python', + `${serverlessId}${pythonServerlessFunctionNames[0]}` + ); + serverlessSummary = response.body; + }); + + it('returns correct memory avg', () => { + expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); + }); + it('returns correct serverless function total', () => { + expect(serverlessSummary.serverlessFunctionsTotal).to.eql(1); + }); + it('returns correct serverless duration avg', () => { + expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); + }); + it('returns correct billed duration avg', () => { + expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json index 5cc5a0032cd31..b1044d198a9a3 100644 --- a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json @@ -4073,10 +4073,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-metric" - }, "mapping": { "total_fields": { "limit": "2000" @@ -4085,8 +4081,7 @@ "max_docvalue_fields_search": "200", "number_of_replicas": "0", "number_of_shards": "1", - "priority": "100", - "refresh_interval": "1ms" + "priority": "100" } } } diff --git a/x-pack/test/apm_api_integration/tests/metrics/metrics_charts.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/metrics_charts.spec.ts deleted file mode 100644 index 92e7c8a80bcb1..0000000000000 --- a/x-pack/test/apm_api_integration/tests/metrics/metrics_charts.spec.ts +++ /dev/null @@ -1,462 +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 expect from '@kbn/expect'; -import { first } from 'lodash'; -import { GenericMetricsChart } from '@kbn/apm-plugin/server/routes/metrics/fetch_and_transform_metrics'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -type ChartResponse = SupertestReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - - registry.when( - 'Metrics charts when data is loaded', - { config: 'basic', archives: ['metrics_8.0.0'] }, - () => { - describe('for opbeans-node', () => { - describe('returns metrics data', () => { - let chartsResponse: ChartResponse; - before(async () => { - chartsResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start: '2020-09-08T14:50:00.000Z', - end: '2020-09-08T14:55:00.000Z', - agentName: 'nodejs', - environment: 'ENVIRONMENT_ALL', - kuery: ``, - }, - }, - }); - }); - - it('contains CPU usage and System memory usage chart data', async () => { - expect(chartsResponse.status).to.be(200); - expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - ] - `); - }); - - describe('CPU usage', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'cpu_usage_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.714, - 0.3877, - 0.75, - 0.2543, - ] - `); - }); - }); - - describe("System memory usage (using 'system.memory' fields to calculate the memory usage)", () => { - let systemMemoryUsageChart: GenericMetricsChart | undefined; - before(() => { - systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - }); - - it('has correct series', () => { - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) - .toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.722093920925555, - 0.718173546796348, - ] - `); - }); - }); - }); - }); - - describe('for opbeans-java', () => { - describe('returns metrics data', () => { - let chartsResponse: ChartResponse; - before(async () => { - chartsResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start: '2020-09-08T14:55:30.000Z', - end: '2020-09-08T15:00:00.000Z', - agentName: 'java', - environment: 'ENVIRONMENT_ALL', - kuery: ``, - }, - }, - }); - }); - - it('has correct chart data', async () => { - expect(chartsResponse.status).to.be(200); - expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - "Heap Memory", - "Non-Heap Memory", - "Thread Count", - "Garbage collection per minute", - "Garbage collection time spent per minute", - ] - `); - }); - - describe('CPU usage', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'cpu_usage_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.203, - 0.178777777777778, - 0.01, - 0.009, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.193, - 0.193, - 0.009, - 0.009, - ] - `); - }); - }); - - describe("System memory usage (using 'system.process.cgroup' fields to calculate the memory usage)", () => { - let systemMemoryUsageChart: GenericMetricsChart | undefined; - before(() => { - systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - }); - - it('has correct series', () => { - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) - .toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.707924703557837, - 0.705395980841182, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.707924703557837, - 0.707924703557837, - ] - `); - }); - }); - - describe('Heap Memory', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'heap_memory_area_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - "Avg. limit", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 222501617.777778, - 374341632, - 1560281088, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 211472896, - 374341632, - 1560281088, - ] - `); - }); - }); - - describe('Non-Heap Memory', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'non_heap_memory_area_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 138573397.333333, - 147677639.111111, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 138162752, - 147386368, - ] - `); - }); - }); - - describe('Thread Count', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'thread_count_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. count", - "Max count", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 44.4444444444444, - 45, - ] - `); - }); - - it('has the correct rate', async () => { - const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 44, - 44, - ] - `); - }); - }); - - describe('Garbage collection per minute', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'gc_rate_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0, - 3, - ] - `); - }); - }); - - describe('Garbage collection time spent per minute', () => { - let cpuUsageChart: GenericMetricsChart | undefined; - before(() => { - cpuUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'gc_time_line_chart' - ); - }); - - it('has correct series', () => { - expect(cpuUsageChart).to.not.empty(); - expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); - }); - - it('has correct series overall values', () => { - expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0, - 37500, - ] - `); - }); - }); - }); - - // 9223372036854771712 = memory limit for a c-group when no memory limit is specified - it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => { - const chartsResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts', - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start: '2020-09-08T15:00:30.000Z', - end: '2020-09-08T15:05:00.000Z', - agentName: 'java', - environment: 'ENVIRONMENT_ALL', - kuery: ``, - }, - }, - }); - - const systemMemoryUsageChart = chartsResponse.body.charts.find( - ({ key }) => key === 'memory_usage_chart' - ); - - expect(systemMemoryUsageChart).to.not.empty(); - expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Max", - "Average", - ] - `); - expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) - .toMatchInline(` - Array [ - 0.114523896426499, - 0.114002376090415, - ] - `); - - const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); - expectSnapshot(yValues).toMatchInline(` - Array [ - 0.11383724014064, - 0.11383724014064, - ] - `); - }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts deleted file mode 100644 index be823d8cc9898..0000000000000 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts +++ /dev/null @@ -1,333 +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 expect from '@kbn/expect'; -import { meanBy, sumBy } from 'lodash'; -import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { generateData, config } from './generate_data'; - -function isNotNullOrZeroCoordinate(coordinate: Coordinate) { - return coordinate.y !== null && coordinate.y !== 0; -} - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - const numberOfTransactionsCreated = 15; - - async function callApi(serviceName: string, serverlessId?: string) { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metrics/serverless/charts', - params: { - path: { serviceName }, - query: { - environment: 'test', - kuery: '', - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - ...(serverlessId ? { serverlessId } : {}), - }, - }, - }); - } - - registry.when( - 'Serverless metrics charts when data is not loaded', - { config: 'basic', archives: [] }, - () => { - let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessMetrics = response.body; - }); - - it('returns empty', () => { - serverlessMetrics.charts.forEach((chart) => { - expect(chart.series).to.be.empty(); - }); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177642 - registry.when('Serverless metrics charts', { config: 'basic', archives: [] }, () => { - const { - memoryTotal, - memoryFree, - billedDurationMs, - coldStartDurationPython, - transactionDuration, - pythonServerlessFunctionNames, - serverlessId, - } = config; - - // eslint-disable-next-line mocha/no-sibling-hooks - before(async () => { - await generateData({ start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('Python service', () => { - let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessMetrics = response.body; - }); - - it('returns all metrics chart', () => { - expect(serverlessMetrics.charts.length).to.be.greaterThan(0); - expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ - 'Cold start duration', - 'Cold starts', - 'Compute usage', - 'Lambda Duration', - 'System memory usage', - ]); - }); - - describe('Avg. Duration', () => { - const transactionDurationInMicroSeconds = transactionDuration * 1000; - [ - { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, - { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const avgDurationMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'avg_duration'; - }); - const series = avgDurationMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; - - describe('Cold start duration', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_duration'; - }); - }); - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); - }); - - it('returns correct mean value', () => { - const meanValue = meanBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(meanValue).to.equal(coldStartDurationPython * 1000); - }); - }); - - describe('Cold start count', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_count'; - }); - }); - - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal( - numberOfTransactionsCreated * pythonServerlessFunctionNames.length - ); - }); - - it('returns correct sum value', () => { - const sumValue = sumBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(sumValue).to.equal( - numberOfTransactionsCreated * pythonServerlessFunctionNames.length - ); - }); - }); - - describe('memory usage', () => { - const expectedFreeMemory = 1 - memoryFree / memoryTotal; - [ - { title: 'Max', expectedValue: expectedFreeMemory }, - { title: 'Average', expectedValue: expectedFreeMemory }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'memory_usage_chart'; - }); - const series = memoryUsageMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - describe('Compute usage', () => { - const GBSeconds = 1024 * 1024 * 1024 * 1000; - let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; - before(() => { - computeUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'compute_usage'; - }); - }); - it('returns correct overall value', () => { - const expectedValue = - ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated * 2; - expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); - }); - - it('returns correct mean value', () => { - const expectedValue = ((memoryTotal * billedDurationMs) / GBSeconds) * 2; - const meanValue = meanBy( - computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), - 'y' - ); - expect(meanValue).to.equal(expectedValue); - }); - }); - }); - - describe('detailed metrics', () => { - let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; - before(async () => { - const response = await callApi( - 'lambda-python', - `${serverlessId}${pythonServerlessFunctionNames[0]}` - ); - serverlessMetrics = response.body; - }); - - it('returns all metrics chart', () => { - expect(serverlessMetrics.charts.length).to.be.greaterThan(0); - expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([ - 'Cold start duration', - 'Cold starts', - 'Compute usage', - 'Lambda Duration', - 'System memory usage', - ]); - }); - - describe('Avg. Duration', () => { - const transactionDurationInMicroSeconds = transactionDuration * 1000; - [ - { title: 'Billed Duration', expectedValue: billedDurationMs * 1000 }, - { title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const avgDurationMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'avg_duration'; - }); - const series = avgDurationMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - let metricsChart: (typeof serverlessMetrics.charts)[0] | undefined; - - describe('Cold start duration', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_duration'; - }); - }); - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000); - }); - - it('returns correct mean value', () => { - const meanValue = meanBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(meanValue).to.equal(coldStartDurationPython * 1000); - }); - }); - - describe('Cold start count', () => { - before(() => { - metricsChart = serverlessMetrics.charts.find((chart) => { - return chart.key === 'cold_start_count'; - }); - }); - - it('returns correct overall value', () => { - expect(metricsChart?.series[0].overallValue).to.equal(numberOfTransactionsCreated); - }); - - it('returns correct sum value', () => { - const sumValue = sumBy( - metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate), - 'y' - ); - expect(sumValue).to.equal(numberOfTransactionsCreated); - }); - }); - - describe('memory usage', () => { - const expectedFreeMemory = 1 - memoryFree / memoryTotal; - [ - { title: 'Max', expectedValue: expectedFreeMemory }, - { title: 'Average', expectedValue: expectedFreeMemory }, - ].map(({ title, expectedValue }) => - it(`returns correct ${title} value`, () => { - const memoryUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'memory_usage_chart'; - }); - const series = memoryUsageMetric?.series.find((item) => item.title === title); - expect(series?.overallValue).to.eql(expectedValue); - const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y'); - expect(meanValue).to.eql(expectedValue); - }) - ); - }); - - describe('Compute usage', () => { - const GBSeconds = 1024 * 1024 * 1024 * 1000; - let computeUsageMetric: (typeof serverlessMetrics.charts)[0] | undefined; - before(() => { - computeUsageMetric = serverlessMetrics.charts.find((chart) => { - return chart.key === 'compute_usage'; - }); - }); - it('returns correct overall value', () => { - const expectedValue = - ((memoryTotal * billedDurationMs) / GBSeconds) * numberOfTransactionsCreated; - expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue); - }); - - it('returns correct mean value', () => { - const expectedValue = (memoryTotal * billedDurationMs) / GBSeconds; - const meanValue = meanBy( - computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0), - 'y' - ); - expect(meanValue).to.equal(expectedValue); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts deleted file mode 100644 index 065597eed1709..0000000000000 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts +++ /dev/null @@ -1,111 +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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { config, expectedValues, generateData } from './generate_data'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - async function callApi(serviceName: string, serverlessId?: string) { - return await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/metrics/serverless/summary`, - params: { - path: { serviceName }, - query: { - environment: 'test', - kuery: '', - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - ...(serverlessId ? { serverlessId } : {}), - }, - }, - }); - } - - registry.when( - 'Serverless overview when data is not loaded', - { config: 'basic', archives: [] }, - () => { - let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessSummary = response.body; - }); - - it('returns empty', () => { - expect(serverlessSummary).to.be.empty(); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177650 - registry.when('Serverless overview', { config: 'basic', archives: [] }, () => { - const { billedDurationMs, pythonServerlessFunctionNames, faasDuration, serverlessId } = config; - const { expectedMemoryUsedRate } = expectedValues; - - // eslint-disable-next-line mocha/no-sibling-hooks - before(async () => { - await generateData({ start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('Python service', () => { - let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; - before(async () => { - const response = await callApi('lambda-python'); - serverlessSummary = response.body; - }); - - it('returns correct memory avg', () => { - expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); - }); - it('returns correct serverless function total', () => { - expect(serverlessSummary.serverlessFunctionsTotal).to.eql( - pythonServerlessFunctionNames.length - ); - }); - it('returns correct serverless duration avg', () => { - expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); - }); - it('returns correct billed duration avg', () => { - expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); - }); - }); - - describe('detailed metrics', () => { - let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; - before(async () => { - const response = await callApi( - 'lambda-python', - `${serverlessId}${pythonServerlessFunctionNames[0]}` - ); - serverlessSummary = response.body; - }); - - it('returns correct memory avg', () => { - expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate); - }); - it('returns correct serverless function total', () => { - expect(serverlessSummary.serverlessFunctionsTotal).to.eql(1); - }); - it('returns correct serverless duration avg', () => { - expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration); - }); - it('returns correct billed duration avg', () => { - expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs); - }); - }); - }); -}