From 7921f5618d6af8e959ed4c52a91a026c672955ea Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Thu, 14 Nov 2024 10:39:46 +0100 Subject: [PATCH 01/23] [APM] Migrate test `apm-api-integration/tests/errors` to be deployment agnostic api tests (#199655) Closes #198971 Part of #193245 ## Summary This PR migrates test apm-api-integration/tests/errors to be deployment agnostic api tests ## Testing 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" ``` --- .../apm/errors/distribution.spec.ts | 207 +++++++++++++++++ .../apm/errors/error_group_list.spec.ts | 153 +++++++++++++ .../apm}/errors/generate_data.ts | 35 ++- .../apm/errors/group_id_samples.spec.ts | 190 ++++++++++++++++ .../apis/observability/apm/errors/index.ts | 20 ++ .../generate_data.ts | 54 +++-- .../top_erroneous_transactions.spec.ts | 211 ++++++++++++++++++ .../generate_data.ts | 47 ++-- .../top_errors_main_stats.spec.ts | 105 +++++++++ .../apis/observability/apm/index.ts | 1 + .../apis/observability/apm/utils/common.ts | 9 +- .../tests/errors/distribution.spec.ts | 203 ----------------- .../tests/errors/error_group_list.spec.ts | 150 ------------- .../tests/errors/group_id_samples.spec.ts | 189 ---------------- .../top_erroneous_transactions.spec.ts | 205 ----------------- .../top_errors_main_stats.spec.ts | 108 --------- 16 files changed, 956 insertions(+), 931 deletions(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/distribution.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/error_group_list.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/errors/generate_data.ts (69%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/group_id_samples.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/errors/top_erroneous_transactions/generate_data.ts (54%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/errors/top_errors_for_transaction/generate_data.ts (58%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/distribution.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/distribution.spec.ts new file mode 100644 index 0000000000000..3c80c8df83018 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/distribution.spec.ts @@ -0,0 +1,207 @@ +/* + * 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, last, sumBy } from 'lodash'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import { config, generateData } from './generate_data'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { isFiniteNumber } from '../utils/common'; + +type ErrorsDistribution = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + 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( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/distribution'>['params'] + > + ) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName, + ...overrides?.path, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + return response; + } + + describe('Error Distributions', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.currentPeriod.length).to.be(0); + expect(response.body.previousPeriod.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + describe('errors distribution', () => { + const { appleTransaction, bananaTransaction } = config; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('without comparison', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const response = await callApi(); + errorsDistribution = response.body; + }); + + it('displays combined number of occurrences', () => { + const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); + const numberOfBuckets = 15; + expect(countSum).to.equal( + (appleTransaction.failureRate + bananaTransaction.failureRate) * numberOfBuckets + ); + }); + + describe('displays correct start in errors distribution chart', () => { + let errorsDistributionWithComparison: ErrorsDistribution; + before(async () => { + const responseWithComparison = await callApi({ + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + offset: '15m', + }, + }); + errorsDistributionWithComparison = responseWithComparison.body; + }); + it('has same start time when comparison is enabled', () => { + expect(first(errorsDistribution.currentPeriod)?.x).to.equal( + first(errorsDistributionWithComparison.currentPeriod)?.x + ); + }); + }); + }); + + describe('displays occurrences for type "apple transaction" only', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const response = await callApi({ + query: { kuery: `error.exception.type:"${appleTransaction.name}"` }, + }); + errorsDistribution = response.body; + }); + it('displays combined number of occurrences', () => { + const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); + const numberOfBuckets = 15; + expect(countSum).to.equal(appleTransaction.failureRate * numberOfBuckets); + }); + }); + + describe('with comparison', () => { + describe('when data is returned', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const fiveMinutes = 5 * 60 * 1000; + const response = await callApi({ + query: { + start: new Date(end - fiveMinutes).toISOString(), + end: new Date(end).toISOString(), + offset: '5m', + }, + }); + errorsDistribution = response.body; + }); + it('returns some data', () => { + const hasCurrentPeriodData = errorsDistribution.currentPeriod.some(({ y }) => + isFiniteNumber(y) + ); + + const hasPreviousPeriodData = errorsDistribution.previousPeriod.some(({ y }) => + isFiniteNumber(y) + ); + + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); + + it('has same start time for both periods', () => { + expect(first(errorsDistribution.currentPeriod)?.x).to.equal( + first(errorsDistribution.previousPeriod)?.x + ); + }); + + it('has same end time for both periods', () => { + expect(last(errorsDistribution.currentPeriod)?.x).to.equal( + last(errorsDistribution.previousPeriod)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + expect(errorsDistribution.currentPeriod.length).to.equal( + errorsDistribution.previousPeriod.length + ); + }); + }); + + describe('when no data is returned', () => { + let errorsDistribution: ErrorsDistribution; + before(async () => { + const response = await callApi({ + query: { + start: '2021-01-03T00:00:00.000Z', + end: '2021-01-03T00:15:00.000Z', + offset: '1d', + }, + }); + errorsDistribution = response.body; + }); + + it('has same start time for both periods', () => { + expect(first(errorsDistribution.currentPeriod)?.x).to.equal( + first(errorsDistribution.previousPeriod)?.x + ); + }); + + it('has same end time for both periods', () => { + expect(last(errorsDistribution.currentPeriod)?.x).to.equal( + last(errorsDistribution.previousPeriod)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + expect(errorsDistribution.currentPeriod.length).to.equal( + errorsDistribution.previousPeriod.length + ); + }); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/error_group_list.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/error_group_list.spec.ts new file mode 100644 index 0000000000000..c99cbfd3df280 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/error_group_list.spec.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +type ErrorGroups = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups']; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + const callApi = async ( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['params'] + > + ) => { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics', + params: { + path: { serviceName, ...overrides?.path }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + }; + describe('Error Group List', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.errorGroups).to.empty(); + }); + }); + + describe('when data is loaded', () => { + describe('errors group', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + const appleTransaction = { + name: 'GET /apple 🍎 ', + successRate: 75, + failureRate: 25, + }; + + const bananaTransaction = { + name: 'GET /banana 🍌', + successRate: 50, + failureRate: 50, + }; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + const serviceInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); + + await apmSynthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(appleTransaction.successRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: appleTransaction.name }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(appleTransaction.failureRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: appleTransaction.name }) + .errors( + serviceInstance.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + timerange(start, end) + .interval('1m') + .rate(bananaTransaction.successRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: bananaTransaction.name }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + timerange(start, end) + .interval('1m') + .rate(bananaTransaction.failureRate) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: bananaTransaction.name }) + .errors( + serviceInstance.error({ message: 'error 2', type: 'bar' }).timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('returns the correct data', () => { + let errorGroups: ErrorGroups; + before(async () => { + const response = await callApi(); + errorGroups = response.body.errorGroups; + }); + + it('returns correct number of errors', () => { + expect(errorGroups.length).to.equal(2); + expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']); + }); + + it('returns correct occurences', () => { + const numberOfBuckets = 15; + expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([ + appleTransaction.failureRate * numberOfBuckets, + bananaTransaction.failureRate * numberOfBuckets, + ]); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/generate_data.ts similarity index 69% rename from x-pack/test/apm_api_integration/tests/errors/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/generate_data.ts index a7e627a048e05..ea22c866bd668 100644 --- a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/generate_data.ts @@ -9,7 +9,7 @@ import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const config = { appleTransaction: { - name: 'GET /apple 🍎 ', + name: 'GET /apple 🍎', successRate: 75, failureRate: 25, }, @@ -35,14 +35,12 @@ export async function generateData({ .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - const interval = '1m'; - const { bananaTransaction, appleTransaction } = config; + const interval = timerange(start, end).interval('1m'); const documents = [appleTransaction, bananaTransaction].flatMap((transaction, index) => { return [ - timerange(start, end) - .interval(interval) + interval .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance @@ -51,21 +49,18 @@ export async function generateData({ .duration(1000) .success() ), - timerange(start, end) - .interval(interval) - .rate(transaction.failureRate) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: transaction.name }) - .errors( - serviceGoProdInstance - .error({ message: `Error ${index}`, type: transaction.name }) - .timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), + interval.rate(transaction.failureRate).generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: transaction.name }) + .errors( + serviceGoProdInstance + .error({ message: `Error ${index}`, type: transaction.name }) + .timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), ]; }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/group_id_samples.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/group_id_samples.spec.ts new file mode 100644 index 0000000000000..9c20c97fde868 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/group_id_samples.spec.ts @@ -0,0 +1,190 @@ +/* + * 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 { timerange } from '@kbn/apm-synthtrace-client'; +import { service } from '@kbn/apm-synthtrace-client/src/lib/apm/service'; +import { orderBy } from 'lodash'; +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { config, generateData } from './generate_data'; + +type ErrorGroupSamples = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples'>; + +type ErrorSampleDetails = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + 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 callErrorGroupSamplesApi({ groupId }: { groupId: string }) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples', + params: { + path: { + serviceName, + groupId, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + return response; + } + + async function callErrorSampleDetailsApi(errorId: string) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}', + params: { + path: { + serviceName, + groupId: 'foo', + errorId, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + return response; + } + describe('Error Group Id Samples', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + it('handles the empty state', async () => { + const response = await callErrorGroupSamplesApi({ groupId: 'foo' }); + expect(response.status).to.be(200); + expect(response.body.occurrencesCount).to.be(0); + }); + + describe('when samples data is loaded', () => { + let errorsSamplesResponse: ErrorGroupSamples; + const { bananaTransaction } = config; + + describe('error group id', () => { + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + const response = await callErrorGroupSamplesApi({ + groupId: '0000000000000000000000000Error 1', + }); + errorsSamplesResponse = response.body; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('displays correct number of occurrences', () => { + const numberOfBuckets = 15; + expect(errorsSamplesResponse.occurrencesCount).to.equal( + bananaTransaction.failureRate * numberOfBuckets + ); + }); + }); + }); + + describe('when error sample data is loaded', () => { + describe('error sample id', () => { + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('return correct data', () => { + let errorSampleDetailsResponse: ErrorSampleDetails; + before(async () => { + const errorsSamplesResponse = await callErrorGroupSamplesApi({ + groupId: '0000000000000000000000000Error 1', + }); + + const errorId = errorsSamplesResponse.body.errorSampleIds[0]; + + const response = await callErrorSampleDetailsApi(errorId); + errorSampleDetailsResponse = response.body; + }); + + it('displays correct error grouping_key', () => { + expect(errorSampleDetailsResponse.error.error.grouping_key).to.equal( + '0000000000000000000000000Error 1' + ); + }); + + it('displays correct error message', () => { + expect(errorSampleDetailsResponse.error.error.exception?.[0].message).to.equal( + 'Error 1' + ); + }); + }); + }); + + describe('with sampled and unsampled transactions', () => { + let errorGroupSamplesResponse: ErrorGroupSamples; + + before(async () => { + const instance = service(serviceName, 'production', 'go').instance('a'); + const errorMessage = 'Error 1'; + const groupId = '0000000000000000000000000Error 1'; + + await apmSynthtraceEsClient.index( + timerange(start, end) + .interval('15m') + .rate(1) + .generator((timestamp) => { + return [ + instance + .transaction('GET /api/foo') + .duration(100) + .timestamp(timestamp) + .sample(false) + .errors( + instance.error({ message: errorMessage }).timestamp(timestamp), + instance.error({ message: errorMessage }).timestamp(timestamp + 1) + ), + instance + .transaction('GET /api/foo') + .duration(100) + .timestamp(timestamp) + .sample(true) + .errors(instance.error({ message: errorMessage }).timestamp(timestamp)), + ]; + }) + ); + + errorGroupSamplesResponse = (await callErrorGroupSamplesApi({ groupId })).body; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns the errors in the correct order (sampled first, then unsampled)', () => { + const idsOfErrors = errorGroupSamplesResponse.errorSampleIds.map((id) => + parseInt(id, 10) + ); + + // this checks whether the order of indexing is different from the order that is returned + // if it is not, scoring/sorting is broken + expect(errorGroupSamplesResponse.errorSampleIds.length).to.be(3); + expect(idsOfErrors).to.not.eql(orderBy(idsOfErrors)); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/index.ts new file mode 100644 index 0000000000000..4061edd217462 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('errors', () => { + loadTestFile(require.resolve('./error_group_list.spec.ts')); + loadTestFile(require.resolve('./group_id_samples.spec.ts')); + loadTestFile(require.resolve('./distribution.spec.ts')); + loadTestFile(require.resolve('./top_errors_for_transaction/top_errors_main_stats.spec.ts')); + loadTestFile( + require.resolve('./top_erroneous_transactions/top_erroneous_transactions.spec.ts') + ); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/generate_data.ts similarity index 54% rename from x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/generate_data.ts index 7732d85efa58f..0a2e690ccf966 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/generate_data.ts @@ -8,15 +8,15 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const config = { - firstTransaction: { - name: 'GET /apple 🍎 ', - successRate: 75, - failureRate: 25, + appleTransaction: { + name: 'GET /apple 🍎', + successRate: 25, + failureRate: 75, }, - secondTransaction: { + bananaTransaction: { name: 'GET /banana 🍌', - successRate: 50, - failureRate: 50, + successRate: 80, + failureRate: 20, }, }; @@ -35,14 +35,12 @@ export async function generateData({ .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - const interval = '1m'; + const { bananaTransaction, appleTransaction } = config; + const interval = timerange(start, end).interval('1m'); - const { firstTransaction, secondTransaction } = config; - - const documents = [firstTransaction, secondTransaction].flatMap((transaction) => { + const documents = [appleTransaction, bananaTransaction].flatMap((transaction, index) => { return [ - timerange(start, end) - .interval(interval) + interval .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance @@ -51,21 +49,21 @@ export async function generateData({ .duration(1000) .success() ), - timerange(start, end) - .interval(interval) - .rate(transaction.failureRate) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: transaction.name }) - .errors( - serviceGoProdInstance - .error({ message: 'Error 1', type: transaction.name }) - .timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), + interval.rate(transaction.failureRate).generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: transaction.name }) + .errors( + serviceGoProdInstance + .error({ message: `Error 1`, type: transaction.name }) + .timestamp(timestamp), + serviceGoProdInstance + .error({ message: `Error 2`, type: transaction.name }) + .timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), ]; }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts new file mode 100644 index 0000000000000..1fb3cdadd8f67 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts @@ -0,0 +1,211 @@ +/* + * 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 { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { sumBy, first, last } from 'lodash'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { config, generateData } from './generate_data'; +import { isFiniteNumber } from '../../utils/common'; + +type ErroneousTransactions = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const groupId = '0000000000000000000000000Error 1'; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>['params'] + > + ) { + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions', + params: { + path: { + serviceName, + groupId, + ...overrides?.path, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + offset: undefined, + numBuckets: 15, + ...overrides?.query, + }, + }, + }); + return response; + } + + describe('Top erroneous transactions', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + it('handles the empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.topErroneousTransactions).to.be.empty(); + }); + + describe('when data is loaded', () => { + const { + appleTransaction: { name: appleTransactionName, failureRate: appleTransactionFailureRate }, + bananaTransaction: { + name: bananaTransactionName, + failureRate: bananaTransactionFailureRate, + }, + } = config; + + describe('returns the correct data', () => { + describe('without comparison', () => { + const numberOfBuckets = 15; + let erroneousTransactions: ErroneousTransactions; + + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + const response = await callApi({ + path: { groupId }, + }); + erroneousTransactions = response.body; + }); + + it('displays the correct number of occurrences', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(topErroneousTransactions.length).to.be(2); + + const bananaTransaction = topErroneousTransactions.find( + (x) => x.transactionName === bananaTransactionName + ); + expect(bananaTransaction).to.not.be(undefined); + expect(bananaTransaction?.occurrences).to.be( + bananaTransactionFailureRate * numberOfBuckets + ); + + const appleTransaction = topErroneousTransactions.find( + (x) => x.transactionName === appleTransactionName + ); + expect(appleTransaction).to.not.be(undefined); + expect(appleTransaction?.occurrences).to.be( + appleTransactionFailureRate * numberOfBuckets + ); + }); + + it('displays the correct number of occurrences in time series', () => { + const { topErroneousTransactions } = erroneousTransactions; + + const bananaTransaction = topErroneousTransactions.find( + (x) => x.transactionName === bananaTransactionName + ); + const firstErrorCount = sumBy(bananaTransaction?.currentPeriodTimeseries, 'y'); + expect(firstErrorCount).to.be(bananaTransactionFailureRate * numberOfBuckets); + + const appleTransaction = topErroneousTransactions.find( + (x) => x.transactionName === appleTransactionName + ); + const secondErrorCount = sumBy(appleTransaction?.currentPeriodTimeseries, 'y'); + expect(secondErrorCount).to.be(appleTransactionFailureRate * numberOfBuckets); + }); + }); + + describe('with comparison', () => { + describe('when there are data for the time periods', () => { + let erroneousTransactions: ErroneousTransactions; + + before(async () => { + const fiveMinutes = 5 * 60 * 1000; + const response = await callApi({ + path: { groupId }, + query: { + start: new Date(end - fiveMinutes).toISOString(), + end: new Date(end).toISOString(), + offset: '5m', + }, + }); + erroneousTransactions = response.body; + }); + + it('returns some data', () => { + const { topErroneousTransactions } = erroneousTransactions; + + const hasCurrentPeriodData = + topErroneousTransactions[0]?.currentPeriodTimeseries.some(({ y }) => + isFiniteNumber(y) + ); + + const hasPreviousPeriodData = + topErroneousTransactions[0]?.previousPeriodTimeseries.some(({ y }) => + isFiniteNumber(y) + ); + + expect(hasCurrentPeriodData).to.be(true); + expect(hasPreviousPeriodData).to.be(true); + }); + + it('has the same start time for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(first(topErroneousTransactions[0]?.currentPeriodTimeseries)?.x).to.be( + first(topErroneousTransactions[0]?.previousPeriodTimeseries)?.x + ); + }); + + it('has same end time for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(last(topErroneousTransactions[0]?.currentPeriodTimeseries)?.x).to.be( + last(topErroneousTransactions[0]?.previousPeriodTimeseries)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + const { topErroneousTransactions } = erroneousTransactions; + expect(topErroneousTransactions[0]?.currentPeriodTimeseries.length).to.be( + topErroneousTransactions[0]?.previousPeriodTimeseries.length + ); + }); + }); + + describe('when there are no data for the time period', () => { + it('returns an empty array', async () => { + const response = await callApi({ + path: { groupId }, + query: { + start: '2021-01-03T00:00:00.000Z', + end: '2021-01-03T00:15:00.000Z', + offset: '1d', + }, + }); + + const { + body: { topErroneousTransactions }, + } = response; + + expect(topErroneousTransactions).to.be.empty(); + }); + }); + }); + }); + }); + + after(() => apmSynthtraceEsClient.clean()); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/generate_data.ts similarity index 58% rename from x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/generate_data.ts index 9f983fbb8877b..0259c70b36448 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/generate_data.ts @@ -8,12 +8,12 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export const config = { - firstTransaction: { + appleTransaction: { name: 'GET /apple 🍎', successRate: 75, failureRate: 25, }, - secondTransaction: { + bananaTransaction: { name: 'GET /banana 🍌', successRate: 50, failureRate: 50, @@ -35,14 +35,12 @@ export async function generateData({ .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - const interval = '1m'; + const { appleTransaction, bananaTransaction } = config; + const interval = timerange(start, end).interval('1m'); - const { firstTransaction, secondTransaction } = config; - - const documents = [firstTransaction, secondTransaction].flatMap((transaction, index) => { + const documents = [appleTransaction, bananaTransaction].flatMap((transaction, index) => { return [ - timerange(start, end) - .interval(interval) + interval .rate(transaction.successRate) .generator((timestamp) => serviceGoProdInstance @@ -51,24 +49,21 @@ export async function generateData({ .duration(1000) .success() ), - timerange(start, end) - .interval(interval) - .rate(transaction.failureRate) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: transaction.name }) - .errors( - serviceGoProdInstance - .error({ message: `Error 1 transaction ${transaction.name}` }) - .timestamp(timestamp), - serviceGoProdInstance - .error({ message: `Error 2 transaction ${transaction.name}` }) - .timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), + interval.rate(transaction.failureRate).generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: transaction.name }) + .errors( + serviceGoProdInstance + .error({ message: `Error 1 transaction ${transaction.name}` }) + .timestamp(timestamp), + serviceGoProdInstance + .error({ message: `Error 2 transaction ${transaction.name}` }) + .timestamp(timestamp) + ) + .duration(1000) + .timestamp(timestamp) + .failure() + ), ]; }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts new file mode 100644 index 0000000000000..10088af2061d5 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import type { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import moment from 'moment'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { config, generateData } from './generate_data'; + +type ErrorGroups = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['errorGroups']; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + 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( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['params'] + > + ) { + return await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name', + params: { + path: { serviceName, ...overrides?.path }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + maxNumberOfErrorGroups: 5, + transactionType: 'request', + transactionName: overrides?.query?.transactionName ?? '', + ...overrides?.query, + }, + }, + }); + } + + describe('Top Errors main stats', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + }); + + it('handles empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.errorGroups).to.empty(); + }); + + describe('when data is loaded', () => { + let errorGroups: ErrorGroups; + const { + appleTransaction: { name: appleTransactionName, failureRate: appleTransactionFailureRate }, + } = config; + describe('top errors for transaction', () => { + before(async () => { + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); + const response = await callApi({ query: { transactionName: appleTransactionName } }); + errorGroups = response.body.errorGroups; + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('returns the correct data', () => { + const NUMBER_OF_BUCKETS = 15; + + it('returns correct number of errors', () => { + expect(errorGroups.length).to.equal(2); + }); + + it('error 1 is correct', () => { + const firstError = errorGroups[0]; + expect(firstError).to.not.be(undefined); + expect(firstError?.name).to.be(`Error 1 transaction GET /apple 🍎`); + expect(firstError?.occurrences).to.be(appleTransactionFailureRate * NUMBER_OF_BUCKETS); + expect(firstError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); + }); + + it('error 2 is correct', () => { + const secondError = errorGroups[1]; + expect(secondError).to.not.be(undefined); + expect(secondError?.name).to.be(`Error 2 transaction GET /apple 🍎`); + expect(secondError?.occurrences).to.be(appleTransactionFailureRate * NUMBER_OF_BUCKETS); + expect(secondError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); + }); + }); + }); + }); + }); +} 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 ff5a4618e33c3..0cf1116f4140a 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 @@ -12,6 +12,7 @@ export default function apmApiIntegrationTests({ }: DeploymentAgnosticFtrProviderContext) { describe('APM', function () { loadTestFile(require.resolve('./agent_explorer')); + loadTestFile(require.resolve('./errors')); loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./mobile')); loadTestFile(require.resolve('./custom_dashboards')); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts index 8f5ff9822e22d..f115945a4ab52 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/utils/common.ts @@ -5,8 +5,13 @@ * 2.0. */ -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { Maybe } from '@kbn/apm-plugin/typings/common'; +import { isFinite } from 'lodash'; +import type { Maybe } from '@kbn/apm-plugin/typings/common'; + +// _.isNumber() returns true for NaN, _.isFinite() does not refine +export function isFiniteNumber(value: any): value is number { + return isFinite(value); +} export function roundNumber(num: Maybe) { return isFiniteNumber(num) ? Number(num.toPrecision(4)) : null; diff --git a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts b/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts deleted file mode 100644 index 544ca97817af0..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts +++ /dev/null @@ -1,203 +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, last, sumBy } from 'lodash'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErrorsDistribution = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const serviceName = 'synth-go'; - 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( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/distribution'>['params'] - > - ) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - ...overrides?.path, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - }, - }, - }); - return response; - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod.length).to.be(0); - expect(response.body.previousPeriod.length).to.be(0); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177336 - registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { - describe('errors distribution', () => { - const { appleTransaction, bananaTransaction } = config; - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('without comparison', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const response = await callApi(); - errorsDistribution = response.body; - }); - - it('displays combined number of occurrences', () => { - const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); - const numberOfBuckets = 15; - expect(countSum).to.equal( - (appleTransaction.failureRate + bananaTransaction.failureRate) * numberOfBuckets - ); - }); - - describe('displays correct start in errors distribution chart', () => { - let errorsDistributionWithComparison: ErrorsDistribution; - before(async () => { - const responseWithComparison = await callApi({ - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - offset: '15m', - }, - }); - errorsDistributionWithComparison = responseWithComparison.body; - }); - it('has same start time when comparison is enabled', () => { - expect(first(errorsDistribution.currentPeriod)?.x).to.equal( - first(errorsDistributionWithComparison.currentPeriod)?.x - ); - }); - }); - }); - - describe('displays occurrences for type "apple transaction" only', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const response = await callApi({ - query: { kuery: `error.exception.type:"${appleTransaction.name}"` }, - }); - errorsDistribution = response.body; - }); - it('displays combined number of occurrences', () => { - const countSum = sumBy(errorsDistribution.currentPeriod, 'y'); - const numberOfBuckets = 15; - expect(countSum).to.equal(appleTransaction.failureRate * numberOfBuckets); - }); - }); - - describe('with comparison', () => { - describe('when data is returned', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const fiveMinutes = 5 * 60 * 1000; - const response = await callApi({ - query: { - start: new Date(end - fiveMinutes).toISOString(), - end: new Date(end).toISOString(), - offset: '5m', - }, - }); - errorsDistribution = response.body; - }); - it('returns some data', () => { - const hasCurrentPeriodData = errorsDistribution.currentPeriod.some(({ y }) => - isFiniteNumber(y) - ); - - const hasPreviousPeriodData = errorsDistribution.previousPeriod.some(({ y }) => - isFiniteNumber(y) - ); - - expect(hasCurrentPeriodData).to.equal(true); - expect(hasPreviousPeriodData).to.equal(true); - }); - - it('has same start time for both periods', () => { - expect(first(errorsDistribution.currentPeriod)?.x).to.equal( - first(errorsDistribution.previousPeriod)?.x - ); - }); - - it('has same end time for both periods', () => { - expect(last(errorsDistribution.currentPeriod)?.x).to.equal( - last(errorsDistribution.previousPeriod)?.x - ); - }); - - it('returns same number of buckets for both periods', () => { - expect(errorsDistribution.currentPeriod.length).to.equal( - errorsDistribution.previousPeriod.length - ); - }); - }); - - describe('when no data is returned', () => { - let errorsDistribution: ErrorsDistribution; - before(async () => { - const response = await callApi({ - query: { - start: '2021-01-03T00:00:00.000Z', - end: '2021-01-03T00:15:00.000Z', - offset: '1d', - }, - }); - errorsDistribution = response.body; - }); - - it('has same start time for both periods', () => { - expect(first(errorsDistribution.currentPeriod)?.x).to.equal( - first(errorsDistribution.previousPeriod)?.x - ); - }); - - it('has same end time for both periods', () => { - expect(last(errorsDistribution.currentPeriod)?.x).to.equal( - last(errorsDistribution.previousPeriod)?.x - ); - }); - - it('returns same number of buckets for both periods', () => { - expect(errorsDistribution.currentPeriod.length).to.equal( - errorsDistribution.previousPeriod.length - ); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts deleted file mode 100644 index fd01833cb4f50..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts +++ /dev/null @@ -1,150 +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 { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -type ErrorGroups = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['errorGroups']; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const serviceName = 'synth-go'; - 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( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>['params'] - > - ) { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics', - params: { - path: { serviceName, ...overrides?.path }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - }, - }, - }); - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.errorGroups).to.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177382 - registry.when('when data is loaded', { config: 'basic', archives: [] }, () => { - describe('errors group', () => { - const appleTransaction = { - name: 'GET /apple 🍎 ', - successRate: 75, - failureRate: 25, - }; - - const bananaTransaction = { - name: 'GET /banana 🍌', - successRate: 50, - failureRate: 50, - }; - - before(async () => { - const serviceInstance = apm - .service({ name: serviceName, environment: 'production', agentName: 'go' }) - .instance('instance-a'); - - await apmSynthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(appleTransaction.successRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: appleTransaction.name }) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(appleTransaction.failureRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: appleTransaction.name }) - .errors( - serviceInstance.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), - timerange(start, end) - .interval('1m') - .rate(bananaTransaction.successRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: bananaTransaction.name }) - .timestamp(timestamp) - .duration(1000) - .success() - ), - timerange(start, end) - .interval('1m') - .rate(bananaTransaction.failureRate) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: bananaTransaction.name }) - .errors( - serviceInstance.error({ message: 'error 2', type: 'bar' }).timestamp(timestamp) - ) - .duration(1000) - .timestamp(timestamp) - .failure() - ), - ]); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('returns the correct data', () => { - let errorGroups: ErrorGroups; - before(async () => { - const response = await callApi(); - errorGroups = response.body.errorGroups; - }); - - it('returns correct number of errors', () => { - expect(errorGroups.length).to.equal(2); - expect(errorGroups.map((error) => error.name).sort()).to.eql(['error 1', 'error 2']); - }); - - it('returns correct occurences', () => { - const numberOfBuckets = 15; - expect(errorGroups.map((error) => error.occurrences).sort()).to.eql([ - appleTransaction.failureRate * numberOfBuckets, - bananaTransaction.failureRate * numberOfBuckets, - ]); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts b/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts deleted file mode 100644 index ea74f1fa622d8..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts +++ /dev/null @@ -1,189 +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 { timerange } from '@kbn/apm-synthtrace-client'; -import { service } from '@kbn/apm-synthtrace-client/src/lib/apm/service'; -import { orderBy } from 'lodash'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { getErrorGroupingKey } from '@kbn/apm-synthtrace-client/src/lib/apm/instance'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErrorGroupSamples = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples'>; - -type ErrorSampleDetails = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const serviceName = 'synth-go'; - 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 callErrorGroupSamplesApi({ groupId }: { groupId: string }) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/samples', - params: { - path: { - serviceName, - groupId, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - return response; - } - - async function callErrorSampleDetailsApi(errorId: string) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/errors/{groupId}/error/{errorId}', - params: { - path: { - serviceName, - groupId: 'foo', - errorId, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - return response; - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callErrorGroupSamplesApi({ groupId: 'foo' }); - expect(response.status).to.be(200); - expect(response.body.occurrencesCount).to.be(0); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177397 - registry.when.skip('when samples data is loaded', { config: 'basic', archives: [] }, () => { - const { bananaTransaction } = config; - describe('error group id', () => { - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('return correct data', () => { - let errorsSamplesResponse: ErrorGroupSamples; - before(async () => { - const response = await callErrorGroupSamplesApi({ - groupId: '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03', - }); - errorsSamplesResponse = response.body; - }); - - it('displays correct number of occurrences', () => { - const numberOfBuckets = 15; - expect(errorsSamplesResponse.occurrencesCount).to.equal( - bananaTransaction.failureRate * numberOfBuckets - ); - }); - }); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177383 - registry.when.skip('when error sample data is loaded', { config: 'basic', archives: [] }, () => { - describe('error sample id', () => { - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('return correct data', () => { - let errorSampleDetailsResponse: ErrorSampleDetails; - before(async () => { - const errorsSamplesResponse = await callErrorGroupSamplesApi({ - groupId: '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03', - }); - - const errorId = errorsSamplesResponse.body.errorSampleIds[0]; - - const response = await callErrorSampleDetailsApi(errorId); - errorSampleDetailsResponse = response.body; - }); - - it('displays correct error grouping_key', () => { - expect(errorSampleDetailsResponse.error.error.grouping_key).to.equal( - '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03' - ); - }); - - it('displays correct error message', () => { - expect(errorSampleDetailsResponse.error.error.exception?.[0].message).to.equal('Error 1'); - }); - }); - }); - - describe('with sampled and unsampled transactions', () => { - let errorGroupSamplesResponse: ErrorGroupSamples; - - before(async () => { - const instance = service(serviceName, 'production', 'go').instance('a'); - const errorMessage = 'Error 1'; - const groupId = getErrorGroupingKey(errorMessage); - - await apmSynthtraceEsClient.index([ - timerange(start, end) - .interval('15m') - .rate(1) - .generator((timestamp) => { - return [ - instance - .transaction('GET /api/foo') - .duration(100) - .timestamp(timestamp) - .sample(false) - .errors( - instance.error({ message: errorMessage }).timestamp(timestamp), - instance.error({ message: errorMessage }).timestamp(timestamp + 1) - ), - instance - .transaction('GET /api/foo') - .duration(100) - .timestamp(timestamp) - .sample(true) - .errors(instance.error({ message: errorMessage }).timestamp(timestamp)), - ]; - }), - ]); - - errorGroupSamplesResponse = (await callErrorGroupSamplesApi({ groupId })).body; - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns the errors in the correct order (sampled first, then unsampled)', () => { - const idsOfErrors = errorGroupSamplesResponse.errorSampleIds.map((id) => parseInt(id, 10)); - - // this checks whether the order of indexing is different from the order that is returned - // if it is not, scoring/sorting is broken - expect(errorGroupSamplesResponse.errorSampleIds.length).to.be(3); - expect(idsOfErrors).to.not.eql(orderBy(idsOfErrors)); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts deleted file mode 100644 index 53b305f093ce4..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts +++ /dev/null @@ -1,205 +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 { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { sumBy, first, last } from 'lodash'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErroneousTransactions = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const serviceName = 'synth-go'; - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; - const groupId = '98b75903135eac35ad42419bd3b45cf8b4270c61cbd0ede0f7e8c8a9ac9fdb03'; - - async function callApi( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions'>['params'] - > - ) { - const response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/errors/{groupId}/top_erroneous_transactions', - params: { - path: { - serviceName, - groupId: 'test', - ...overrides?.path, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - offset: undefined, - numBuckets: 15, - ...overrides?.query, - }, - }, - }); - return response; - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.topErroneousTransactions).to.be.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177637 - registry.when.skip('when data is loaded', { config: 'basic', archives: [] }, () => { - const { - firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate }, - secondTransaction: { name: secondTransactionName, failureRate: secondTransactionFailureRate }, - } = config; - - describe('returns the correct data', () => { - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('without comparison', () => { - const numberOfBuckets = 15; - let erroneousTransactions: ErroneousTransactions; - - before(async () => { - const response = await callApi({ - path: { groupId }, - }); - erroneousTransactions = response.body; - }); - - it.skip('displays the correct number of occurrences', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(topErroneousTransactions.length).to.be(2); - - const firstTransaction = topErroneousTransactions.find( - (x) => x.transactionName === firstTransactionName - ); - expect(firstTransaction).to.not.be(undefined); - expect(firstTransaction?.occurrences).to.be( - firstTransactionFailureRate * numberOfBuckets - ); - - const secondTransaction = topErroneousTransactions.find( - (x) => x.transactionName === secondTransactionName - ); - expect(secondTransaction).to.not.be(undefined); - expect(secondTransaction?.occurrences).to.be( - secondTransactionFailureRate * numberOfBuckets - ); - }); - - it('displays the correct number of occurrences in time series', () => { - const { topErroneousTransactions } = erroneousTransactions; - - const firstTransaction = topErroneousTransactions.find( - (x) => x.transactionName === firstTransactionName - ); - const firstErrorCount = sumBy(firstTransaction?.currentPeriodTimeseries, 'y'); - expect(firstErrorCount).to.be(firstTransactionFailureRate * numberOfBuckets); - - const secondTransaction = topErroneousTransactions.find( - (x) => x.transactionName === secondTransactionName - ); - const secondErrorCount = sumBy(secondTransaction?.currentPeriodTimeseries, 'y'); - expect(secondErrorCount).to.be(secondTransactionFailureRate * numberOfBuckets); - }); - }); - - describe('with comparison', () => { - describe('when there are data for the time periods', () => { - let erroneousTransactions: ErroneousTransactions; - - before(async () => { - const fiveMinutes = 5 * 60 * 1000; - const response = await callApi({ - path: { groupId }, - query: { - start: new Date(end - fiveMinutes).toISOString(), - end: new Date(end).toISOString(), - offset: '5m', - }, - }); - erroneousTransactions = response.body; - }); - - it('returns some data', () => { - const { topErroneousTransactions } = erroneousTransactions; - - const hasCurrentPeriodData = topErroneousTransactions[0].currentPeriodTimeseries.some( - ({ y }) => isFiniteNumber(y) - ); - - const hasPreviousPeriodData = topErroneousTransactions[0].previousPeriodTimeseries.some( - ({ y }) => isFiniteNumber(y) - ); - - expect(hasCurrentPeriodData).to.be(true); - expect(hasPreviousPeriodData).to.be(true); - }); - - it('has the same start time for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(first(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( - first(topErroneousTransactions[0].previousPeriodTimeseries)?.x - ); - }); - - it('has same end time for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(last(topErroneousTransactions[0].currentPeriodTimeseries)?.x).to.be( - last(topErroneousTransactions[0].previousPeriodTimeseries)?.x - ); - }); - - it('returns same number of buckets for both periods', () => { - const { topErroneousTransactions } = erroneousTransactions; - expect(topErroneousTransactions[0].currentPeriodTimeseries.length).to.be( - topErroneousTransactions[0].previousPeriodTimeseries.length - ); - }); - }); - - describe('when there are no data for the time period', () => { - it('returns an empty array', async () => { - const response = await callApi({ - path: { groupId }, - query: { - start: '2021-01-03T00:00:00.000Z', - end: '2021-01-03T00:15:00.000Z', - offset: '1d', - }, - }); - - const { - body: { topErroneousTransactions }, - } = response; - - expect(topErroneousTransactions).to.be.empty(); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts deleted file mode 100644 index a6476e76a3918..0000000000000 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts +++ /dev/null @@ -1,108 +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 { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import moment from 'moment'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { config, generateData } from './generate_data'; - -type ErrorGroups = - APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['errorGroups']; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const serviceName = 'synth-go'; - 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( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name'>['params'] - > - ) { - return await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics_by_transaction_name', - params: { - path: { serviceName, ...overrides?.path }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - maxNumberOfErrorGroups: 5, - transactionType: 'request', - transactionName: '', - ...overrides?.query, - }, - }, - }); - } - - registry.when('when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.errorGroups).to.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177638 - registry.when.skip('when data is loaded', { config: 'basic', archives: [] }, () => { - describe('top errors for transaction', () => { - const { - firstTransaction: { name: firstTransactionName, failureRate: firstTransactionFailureRate }, - } = config; - - before(async () => { - await generateData({ serviceName, start, end, apmSynthtraceEsClient }); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('returns the correct data', () => { - const NUMBER_OF_BUCKETS = 15; - let errorGroups: ErrorGroups; - before(async () => { - const response = await callApi({ query: { transactionName: firstTransactionName } }); - errorGroups = response.body.errorGroups; - }); - - it('returns correct number of errors', () => { - expect(errorGroups.length).to.equal(2); - }); - - it('error 1 is correct', () => { - const firstErrorId = `b6c1d4d41b0b60b841f40232497344ba36856fcbea0692a4695562ca73e790bd`; - const firstError = errorGroups.find((x) => x.groupId === firstErrorId); - expect(firstError).to.not.be(undefined); - expect(firstError?.groupId).to.be(firstErrorId); - expect(firstError?.name).to.be(`Error 1 transaction GET /apple 🍎`); - expect(firstError?.occurrences).to.be(firstTransactionFailureRate * NUMBER_OF_BUCKETS); - expect(firstError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); - }); - - it('error 2 is correct', () => { - const secondErrorId = `c3f388e4f7276d4fab85aa2fad2d2a42e70637f65cd5ec9f085de28b36e69ba5`; - const secondError = errorGroups.find((x) => x.groupId === secondErrorId); - expect(secondError).to.not.be(undefined); - expect(secondError?.groupId).to.be(secondErrorId); - expect(secondError?.name).to.be(`Error 2 transaction GET /apple 🍎`); - expect(secondError?.occurrences).to.be(firstTransactionFailureRate * NUMBER_OF_BUCKETS); - expect(secondError?.lastSeen).to.be(moment(end).startOf('minute').valueOf()); - }); - }); - }); - }); -} From 68d95e424ba5ff9163a97fa11b31ca96acccd50e Mon Sep 17 00:00:00 2001 From: Alex Szabo Date: Thu, 14 Nov 2024 10:46:53 +0100 Subject: [PATCH 02/23] [CI] Fix on-merge pipeline syntax (#200129) ## Summary use soft_fail instead of continue_on_failure, as this setting doesn't exist on command steps (https://buildkite.com/docs/pipelines/command-step) --- .buildkite/pipelines/on_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 87bb71c312688..66cc3f9f33042 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -502,7 +502,7 @@ steps: machineType: n2-standard-2 preemptible: true timeout_in_minutes: 60 - continue_on_failure: true + soft_fail: true retry: automatic: - exit_status: '-1' From 9d38922401d0bbd0d95d750f68fec77ca22758fb Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Thu, 14 Nov 2024 11:00:38 +0100 Subject: [PATCH 03/23] [One Discover] Add app menu actions for Observability projects (#198987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Closes #182230 This work introduces a new observability root profile and uses the new extension point to register custom actions on the app menu. The registered actions and link will appear only with the new project navigation enabled on an Observability project: - A link to the data sets quality page - On the alerts sub menu... - replace the default search rule creation with the observability custom threshold rule - add an entry to directly create an SLO for the current search To access the SLO capabilities without breaking the dependencies hierarchy of the new sustainable architecture, the feature is registered by the common plugin `discover-shared` in SLO and consumed then by Discover using the IoC principle. ## 🖼️ Screenshots ### Observability project solution - show new menu Screenshot 2024-11-06 at 12 37 02 ### Search project solution - hide new menu Screenshot 2024-11-06 at 12 36 19 ### Default navigation mode - hide new menu Screenshot 2024-11-06 at 12 35 43 ## 🎥 Demo https://github.com/user-attachments/assets/104e6074-0401-4fd2-a8e6-8b05f2c070d7 --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/discover/kibana.jsonc | 5 +- src/plugins/discover/public/build_services.ts | 3 + .../accessors/get_app_menu.ts | 161 ++++++++++++++++++ .../accessors/index.ts | 10 ++ .../observability_root_profile/index.ts | 10 ++ .../profile.test.ts | 51 ++++++ .../observability_root_profile/profile.tsx | 28 +++ .../register_profile_providers.ts | 2 + src/plugins/discover/public/types.ts | 2 + src/plugins/discover/tsconfig.json | 3 +- .../services/discover_features/types.ts | 10 +- .../observability_solution/slo/kibana.jsonc | 1 + .../slo/public/plugin.ts | 16 +- .../slo/public/types.ts | 2 + .../utils/get_lazy_with_context_providers.tsx | 5 +- .../observability_solution/slo/tsconfig.json | 1 + .../discover/search_source_alert.ts | 2 +- .../context_awareness/_get_app_menu.ts | 81 +++++++++ .../discover/context_awareness/index.ts | 40 +++++ .../test_suites/observability/index.ts | 1 + 20 files changed, 424 insertions(+), 10 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx create mode 100644 x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts create mode 100644 x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/index.ts diff --git a/src/plugins/discover/kibana.jsonc b/src/plugins/discover/kibana.jsonc index 87837a38ed834..f605d0ae1df95 100644 --- a/src/plugins/discover/kibana.jsonc +++ b/src/plugins/discover/kibana.jsonc @@ -15,6 +15,7 @@ "charts", "data", "dataViews", + "discoverShared", "embeddable", "inspector", "fieldFormats", @@ -30,7 +31,7 @@ "unifiedDocViewer", "unifiedSearch", "unifiedHistogram", - "contentManagement" + "contentManagement", ], "optionalPlugins": [ "dataVisualizer", @@ -46,7 +47,7 @@ "observabilityAIAssistant", "aiops", "fieldsMetadata", - "logsDataAccess" + "logsDataAccess", ], "requiredBundles": [ "kibanaUtils", diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index bae2907af7699..df194bc03fa0f 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -59,6 +59,7 @@ import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; +import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import type { DiscoverStartPlugins } from './types'; import type { DiscoverContextAppLocator } from './application/context/services/locator'; import type { DiscoverSingleDocLocator } from './application/doc/locator'; @@ -89,6 +90,7 @@ export interface DiscoverServices { chrome: ChromeStart; core: CoreStart; data: DataPublicPluginStart; + discoverShared: DiscoverSharedPublicStart; docLinks: DocLinksStart; embeddable: EmbeddableStart; history: History; @@ -178,6 +180,7 @@ export const buildServices = memoize( core, data: plugins.data, dataVisualizer: plugins.dataVisualizer, + discoverShared: plugins.discoverShared, docLinks: core.docLinks, embeddable: plugins.embeddable, i18n: core.i18n, diff --git a/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts new file mode 100644 index 0000000000000..759765e93767f --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts @@ -0,0 +1,161 @@ +/* + * 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 { AppMenuActionId, AppMenuActionType, AppMenuRegistry } from '@kbn/discover-utils'; +import { DATA_QUALITY_LOCATOR_ID, DataQualityLocatorParams } from '@kbn/deeplinks-observability'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { isOfQueryType } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { AppMenuExtensionParams } from '../../../..'; +import type { RootProfileProvider } from '../../../../profiles'; +import { ProfileProviderServices } from '../../../profile_provider_services'; + +export const createGetAppMenu = + (services: ProfileProviderServices): RootProfileProvider['profile']['getAppMenu'] => + (prev) => + (params) => { + const prevValue = prev(params); + + return { + appMenuRegistry: (registry) => { + // Register custom link actions + registerDatasetQualityLink(registry, services); + // Register alerts sub menu actions + registerCreateSLOAction(registry, services, params); + registerCustomThresholdRuleAction(registry, services, params); + + return prevValue.appMenuRegistry(registry); + }, + }; + }; + +const registerDatasetQualityLink = ( + registry: AppMenuRegistry, + { share, timefilter }: ProfileProviderServices +) => { + const dataQualityLocator = + share?.url.locators.get(DATA_QUALITY_LOCATOR_ID); + + if (dataQualityLocator) { + registry.registerCustomAction({ + id: 'dataset-quality-link', + type: AppMenuActionType.custom, + controlProps: { + label: i18n.translate('discover.observabilitySolution.appMenu.datasets', { + defaultMessage: 'Data sets', + }), + testId: 'discoverAppMenuDatasetQualityLink', + onClick: ({ onFinishAction }) => { + const refresh = timefilter.getRefreshInterval(); + const { from, to } = timefilter.getTime(); + + dataQualityLocator.navigate({ + filters: { + timeRange: { + from: from ?? 'now-24h', + to: to ?? 'now', + refresh, + }, + }, + }); + + onFinishAction(); + }, + }, + }); + } +}; + +const registerCustomThresholdRuleAction = ( + registry: AppMenuRegistry, + { data, triggersActionsUi }: ProfileProviderServices, + { dataView }: AppMenuExtensionParams +) => { + registry.registerCustomActionUnderSubmenu(AppMenuActionId.alerts, { + id: AppMenuActionId.createRule, + type: AppMenuActionType.custom, + order: 101, + controlProps: { + label: i18n.translate('discover.observabilitySolution.appMenu.customThresholdRule', { + defaultMessage: 'Create custom threshold rule', + }), + iconType: 'visGauge', + testId: 'discoverAppMenuCustomThresholdRule', + onClick: ({ onFinishAction }) => { + const index = dataView?.toMinimalSpec(); + const { filters, query } = data.query.getState(); + + return triggersActionsUi.getAddRuleFlyout({ + consumer: 'logs', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + canChangeTrigger: false, + initialValues: { + params: { + searchConfiguration: { + index, + query, + filter: filters, + }, + }, + }, + onClose: onFinishAction, + }); + }, + }, + }); +}; + +const registerCreateSLOAction = ( + registry: AppMenuRegistry, + { data, discoverShared }: ProfileProviderServices, + { dataView, isEsqlMode }: AppMenuExtensionParams +) => { + const sloFeature = discoverShared.features.registry.getById('observability-create-slo'); + + if (sloFeature) { + registry.registerCustomActionUnderSubmenu(AppMenuActionId.alerts, { + id: 'create-slo', + type: AppMenuActionType.custom, + order: 102, + controlProps: { + label: i18n.translate('discover.observabilitySolution.appMenu.slo', { + defaultMessage: 'Create SLO', + }), + iconType: 'bell', + testId: 'discoverAppMenuCreateSlo', + onClick: ({ onFinishAction }) => { + const index = dataView?.getIndexPattern(); + const timestampField = dataView?.timeFieldName; + const { filters, query: kqlQuery } = data.query.getState(); + + const filter = isEsqlMode + ? {} + : { + kqlQuery: isOfQueryType(kqlQuery) ? kqlQuery.query : '', + filters: filters?.map(({ meta, query }) => ({ meta, query })), + }; + + return sloFeature.createSLOFlyout({ + initialValues: { + indicator: { + type: 'sli.kql.custom', + params: { + index, + timestampField, + filter, + }, + }, + }, + onClose: onFinishAction, + }); + }, + }, + }); + } +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/index.ts new file mode 100644 index 0000000000000..4a719e634621e --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/index.ts @@ -0,0 +1,10 @@ +/* + * 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". + */ + +export { createGetAppMenu } from './get_app_menu'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts new file mode 100644 index 0000000000000..ad888b42b5610 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/index.ts @@ -0,0 +1,10 @@ +/* + * 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". + */ + +export { createObservabilityRootProfileProvider } from './profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts new file mode 100644 index 0000000000000..b83afb266f2bd --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createObservabilityRootProfileProvider } from './profile'; + +const mockServices = createContextAwarenessMocks().profileProviderServices; + +describe('observabilityRootProfileProvider', () => { + const observabilityRootProfileProvider = createObservabilityRootProfileProvider(mockServices); + const RESOLUTION_MATCH = { + isMatch: true, + context: { solutionType: SolutionType.Observability }, + }; + const RESOLUTION_MISMATCH = { + isMatch: false, + }; + + it('should match when the solution project is observability', () => { + expect( + observabilityRootProfileProvider.resolve({ + solutionNavId: SolutionType.Observability, + }) + ).toEqual(RESOLUTION_MATCH); + }); + + it('should NOT match when the solution project anything but observability', () => { + expect( + observabilityRootProfileProvider.resolve({ + solutionNavId: SolutionType.Default, + }) + ).toEqual(RESOLUTION_MISMATCH); + expect( + observabilityRootProfileProvider.resolve({ + solutionNavId: SolutionType.Search, + }) + ).toEqual(RESOLUTION_MISMATCH); + expect( + observabilityRootProfileProvider.resolve({ + solutionNavId: SolutionType.Security, + }) + ).toEqual(RESOLUTION_MISMATCH); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx new file mode 100644 index 0000000000000..d4b10c8d0a095 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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 { RootProfileProvider, SolutionType } from '../../../profiles'; +import { ProfileProviderServices } from '../../profile_provider_services'; +import { createGetAppMenu } from './accessors'; + +export const createObservabilityRootProfileProvider = ( + services: ProfileProviderServices +): RootProfileProvider => ({ + profileId: 'observability-root-profile', + profile: { + getAppMenu: createGetAppMenu(services), + }, + resolve: (params) => { + if (params.solutionNavId === SolutionType.Observability) { + return { isMatch: true, context: { solutionType: SolutionType.Observability } }; + } + + return { isMatch: false }; + }, +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts index 997edac1bae57..cb4146d1b99bb 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts @@ -27,6 +27,7 @@ import { ProfileProviderServices, } from './profile_provider_services'; import type { DiscoverServices } from '../../build_services'; +import { createObservabilityRootProfileProvider } from './observability/observability_root_profile'; /** * Register profile providers for root, data source, and document contexts to the profile profile services @@ -122,6 +123,7 @@ const createRootProfileProviders = (providerServices: ProfileProviderServices) = createExampleRootProfileProvider(), createExampleSolutionViewRootProfileProvider(), createSecurityRootProfileProvider(providerServices), + createObservabilityRootProfileProvider(providerServices), ]; /** diff --git a/src/plugins/discover/public/types.ts b/src/plugins/discover/public/types.ts index 3b24341e1a654..2ef380db98703 100644 --- a/src/plugins/discover/public/types.ts +++ b/src/plugins/discover/public/types.ts @@ -42,6 +42,7 @@ import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; +import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import { DiscoverAppLocator } from '../common'; import { DiscoverCustomizationContext } from './customizations'; import { type DiscoverContainerProps } from './components/discover_container'; @@ -151,6 +152,7 @@ export interface DiscoverStartPlugins { dataViewFieldEditor: IndexPatternFieldEditorStart; dataViews: DataViewsServicePublic; dataVisualizer?: DataVisualizerPluginStart; + discoverShared: DiscoverSharedPublicStart; embeddable: EmbeddableStart; expressions: ExpressionsStart; fieldFormats: FieldFormatsStart; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 72d5594ba40f0..1bb3aa10acce0 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -98,7 +98,8 @@ "@kbn/logs-data-access-plugin", "@kbn/core-lifecycle-browser", "@kbn/discover-contextual-components", - "@kbn/esql-ast" + "@kbn/esql-ast", + "@kbn/discover-shared-plugin" ], "exclude": [ "target/**/*" diff --git a/src/plugins/discover_shared/public/services/discover_features/types.ts b/src/plugins/discover_shared/public/services/discover_features/types.ts index ebfa3970e96e4..cdf78b3335507 100644 --- a/src/plugins/discover_shared/public/services/discover_features/types.ts +++ b/src/plugins/discover_shared/public/services/discover_features/types.ts @@ -30,8 +30,16 @@ export interface ObservabilityLogsAIAssistantFeature { render: (deps: ObservabilityLogsAIAssistantFeatureRenderDeps) => JSX.Element; } +export interface ObservabilityCreateSLOFeature { + id: 'observability-create-slo'; + createSLOFlyout: (props: { + onClose: () => void; + initialValues: Record; + }) => React.ReactNode; +} + // This should be a union of all the available client features. -export type DiscoverFeature = ObservabilityLogsAIAssistantFeature; +export type DiscoverFeature = ObservabilityLogsAIAssistantFeature | ObservabilityCreateSLOFeature; /** * Service types diff --git a/x-pack/plugins/observability_solution/slo/kibana.jsonc b/x-pack/plugins/observability_solution/slo/kibana.jsonc index e5732ee25e7e1..79302b58f8269 100644 --- a/x-pack/plugins/observability_solution/slo/kibana.jsonc +++ b/x-pack/plugins/observability_solution/slo/kibana.jsonc @@ -22,6 +22,7 @@ "dashboard", "data", "dataViews", + "discoverShared", "lens", "dataViewEditor", "dataViewFieldEditor", diff --git a/x-pack/plugins/observability_solution/slo/public/plugin.ts b/x-pack/plugins/observability_solution/slo/public/plugin.ts index e61910e108a7d..9a1b5f3267b86 100644 --- a/x-pack/plugins/observability_solution/slo/public/plugin.ts +++ b/x-pack/plugins/observability_solution/slo/public/plugin.ts @@ -198,6 +198,7 @@ export class SLOPlugin public start(core: CoreStart, plugins: SLOPublicPluginsStart) { const kibanaVersion = this.initContext.env.packageInfo.version; + const sloClient = createRepositoryClient(core); const lazyWithContextProviders = getLazyWithContextProviders({ @@ -212,11 +213,18 @@ export class SLOPlugin sloClient, }); + const getCreateSLOFlyout = lazyWithContextProviders( + lazy(() => import('./pages/slo_edit/shared_flyout/slo_add_form_flyout')), + { spinnerSize: 'm' } + ); + + plugins.discoverShared.features.registry.register({ + id: 'observability-create-slo', + createSLOFlyout: getCreateSLOFlyout, + }); + return { - getCreateSLOFlyout: lazyWithContextProviders( - lazy(() => import('./pages/slo_edit/shared_flyout/slo_add_form_flyout')), - { spinnerSize: 'm' } - ), + getCreateSLOFlyout, }; } diff --git a/x-pack/plugins/observability_solution/slo/public/types.ts b/x-pack/plugins/observability_solution/slo/public/types.ts index 2c66b340df6d3..1397b08a29528 100644 --- a/x-pack/plugins/observability_solution/slo/public/types.ts +++ b/x-pack/plugins/observability_solution/slo/public/types.ts @@ -14,6 +14,7 @@ import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DiscoverStart } from '@kbn/discover-plugin/public'; +import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; @@ -77,6 +78,7 @@ export interface SLOPublicPluginsStart { dataViewFieldEditor: DataViewFieldEditorStart; dataViews: DataViewsPublicPluginStart; discover?: DiscoverStart; + discoverShared: DiscoverSharedPublicStart; embeddable: EmbeddableStart; fieldFormats: FieldFormatsStart; lens: LensPublicStart; diff --git a/x-pack/plugins/observability_solution/slo/public/utils/get_lazy_with_context_providers.tsx b/x-pack/plugins/observability_solution/slo/public/utils/get_lazy_with_context_providers.tsx index a43aa9e7bff59..49bb461b97af0 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/get_lazy_with_context_providers.tsx +++ b/x-pack/plugins/observability_solution/slo/public/utils/get_lazy_with_context_providers.tsx @@ -47,7 +47,10 @@ export const getLazyWithContextProviders = experimentalFeatures, sloClient, }: Props) => - (LazyComponent: React.LazyExoticComponent, options?: Options): React.FunctionComponent => { + >( + LazyComponent: React.LazyExoticComponent, + options?: Options + ): React.FunctionComponent> => { const { spinnerSize = 'xl' } = options ?? {}; const queryClient = new QueryClient(); return (props) => ( diff --git a/x-pack/plugins/observability_solution/slo/tsconfig.json b/x-pack/plugins/observability_solution/slo/tsconfig.json index 4b05b5aa0b063..23efcc39698b1 100644 --- a/x-pack/plugins/observability_solution/slo/tsconfig.json +++ b/x-pack/plugins/observability_solution/slo/tsconfig.json @@ -96,6 +96,7 @@ "@kbn/core-theme-browser", "@kbn/ebt-tools", "@kbn/observability-alerting-rule-utils", + "@kbn/discover-shared-plugin", "@kbn/server-route-repository-client", "@kbn/server-route-repository-utils" ] diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index 69db169209d59..d4653ca02f6f1 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -367,7 +367,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Search source Alert', function () { // see details: https://github.com/elastic/kibana/issues/193842 - this.tags(['failsOnMKI']); + this.tags(['failsOnMKI', 'skipSvlOblt']); before(async () => { await security.testUser.setRoles(['discover_alert']); await PageObjects.svlCommonPage.loginAsAdmin(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts new file mode 100644 index 0000000000000..2be17df28d12f --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts @@ -0,0 +1,81 @@ +/* + * 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 kbnRison from '@kbn/rison'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common, svlCommonPage } = getPageObjects([ + 'common', + 'timePicker', + 'discover', + 'header', + 'timePicker', + 'svlCommonPage', + ]); + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + describe('extension getAppMenu', () => { + before(async () => { + await svlCommonPage.loginAsAdmin(); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + beforeEach(async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from logstash* | sort @timestamp desc' }, + }); + await common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + }); + + it('should display a "Add data" link to navigate to the onboading page', async () => { + const link = await testSubjects.find('discoverAppMenuDatasetQualityLink'); + await link.click(); + + await retry.try(async () => { + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/app/management/data/data_quality`); + }); + }); + + it('should display a "Create custom threshold rule" action under the Alerts menu to create an o11y alert', async () => { + const alertsButton = await testSubjects.find('discoverAlertsButton'); + await alertsButton.click(); + + const createRuleButton = await testSubjects.find('discoverAppMenuCustomThresholdRule'); + await createRuleButton.click(); + + const ruleTitleElement = await testSubjects.find('selectedRuleTypeTitle'); + + await retry.try(async () => { + expect(await ruleTitleElement.getVisibleText()).to.equal('Custom threshold'); + }); + }); + + it('should display a "Create SLO" action under the Alerts menu to create an o11y alert', async () => { + const alertsButton = await testSubjects.find('discoverAlertsButton'); + await alertsButton.click(); + + const createSLOButton = await testSubjects.find('discoverAppMenuCreateSlo'); + await createSLOButton.click(); + + const sloTitleElement = await testSubjects.find('addSLOFlyoutTitle'); + expect(await sloTitleElement.getVisibleText()).to.equal('Create SLO'); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/index.ts b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/index.ts new file mode 100644 index 0000000000000..c8277b273f428 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/index.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker', 'svlCommonPage']); + const from = '2024-06-10T14:00:00.000Z'; + const to = '2024-06-10T16:30:00.000Z'; + + describe('discover/observabilitySolution/context_awareness', function () { + this.tags(['esGate']); + + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/discover/context_awareness'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/discover/context_awareness' + ); + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': `{ "from": "${from}", "to": "${to}"}`, + }); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/discover/context_awareness'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/discover/context_awareness' + ); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + }); + + loadTestFile(require.resolve('./_get_app_menu')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/index.ts b/x-pack/test_serverless/functional/test_suites/observability/index.ts index 566f2b8e6854e..0885a319636b1 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/index.ts @@ -15,6 +15,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./navigation')); loadTestFile(require.resolve('./observability_logs_explorer')); loadTestFile(require.resolve('./dataset_quality')); + loadTestFile(require.resolve('./discover/context_awareness')); loadTestFile(require.resolve('./onboarding')); loadTestFile(require.resolve('./rules/rules_list')); loadTestFile(require.resolve('./cases')); From 560ae9ab3095e55a0949e5853cd510e47b77dab8 Mon Sep 17 00:00:00 2001 From: Bena Kansara <69037875+benakansara@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:39:21 +0100 Subject: [PATCH 04/23] [Observability] Migrate from the legacy Find alert API to the new one (#199877) Resolves https://github.com/elastic/kibana/issues/192873 --- .../public/context/has_data_context/get_observability_alerts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/observability/public/context/has_data_context/get_observability_alerts.ts b/x-pack/plugins/observability_solution/observability/public/context/has_data_context/get_observability_alerts.ts index 57cc6cec10c83..6fb1fa66731b4 100644 --- a/x-pack/plugins/observability_solution/observability/public/context/has_data_context/get_observability_alerts.ts +++ b/x-pack/plugins/observability_solution/observability/public/context/has_data_context/get_observability_alerts.ts @@ -13,7 +13,7 @@ const allowedConsumers = ['apm', 'uptime', 'logs', 'infrastructure', 'alerts']; export async function getObservabilityAlerts({ http }: { http: HttpSetup }) { try { const { data = [] }: { data: Rule[] } = - (await http.get('/api/alerts/_find', { + (await http.get('/api/alerting/rules/_find', { query: { page: 1, per_page: 20, From ad56ec5f1a838486d9b38c2d5aa92bbf77e127a3 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Thu, 14 Nov 2024 12:49:21 +0100 Subject: [PATCH 05/23] Sustainable Kibana Architecture: Remove dependencies between plugins that are related by _App Links_ (#199492) ## Summary This PR introduces a Core API to check whether a given application has been registered. Plugins can use this for their _App Links_, without having to depend on the referenced plugin(s) anymore. This way, we can get rid of some inter-solution dependencies, and categorise plugins more appropriately. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/application_service.test.ts | 30 +- .../src/application_service.tsx | 3 + .../src/application_service.mock.ts | 2 + .../core-application-browser/src/contracts.ts | 15 +- .../src/plugin_context.ts | 1 + .../public/__mocks__/start_contract.ts | 1 + .../saved_objects_table.test.tsx.snap | 1 + x-pack/plugins/enterprise_search/kibana.jsonc | 8 +- .../enterprise_search/public/plugin.ts | 4 +- .../fleet/.storybook/context/application.ts | 1 + .../kibana.jsonc | 1 - .../public/plugin.ts | 10 +- .../components/search_connector_tab.tsx | 2 +- .../routes/components/settings_page.tsx | 5 +- .../tsconfig.json | 1 - .../search_inference_endpoints/kibana.jsonc | 7 +- x-pack/plugins/search_playground/kibana.jsonc | 7 +- x-pack/plugins/serverless_search/kibana.jsonc | 1 - .../public/navigation_tree.ts | 331 +++++++++--------- .../serverless_search/public/plugin.ts | 2 +- .../plugins/serverless_search/public/types.ts | 2 - .../plugins/serverless_search/tsconfig.json | 2 +- .../.storybook/context/application.tsx | 1 + 23 files changed, 242 insertions(+), 196 deletions(-) diff --git a/packages/core/application/core-application-browser-internal/src/application_service.test.ts b/packages/core/application/core-application-browser-internal/src/application_service.test.ts index 8cd9a61923240..13b2317605ad0 100644 --- a/packages/core/application/core-application-browser-internal/src/application_service.test.ts +++ b/packages/core/application/core-application-browser-internal/src/application_service.test.ts @@ -26,14 +26,14 @@ import { themeServiceMock } from '@kbn/core-theme-browser-mocks'; import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks'; import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks'; import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks'; -import { MockLifecycle } from './test_helpers/test_types'; +import type { MockLifecycle } from './test_helpers/test_types'; import { ApplicationService } from './application_service'; import { - App, - AppDeepLink, + type App, + type AppDeepLink, AppStatus, - AppUpdater, - PublicAppInfo, + type AppUpdater, + type PublicAppInfo, } from '@kbn/core-application-browser'; import { act } from 'react-dom/test-utils'; import { DEFAULT_APP_VISIBILITY } from './utils'; @@ -618,6 +618,26 @@ describe('#start()', () => { }); }); + describe('isAppRegistered', () => { + let isAppRegistered: any; + beforeEach(async () => { + const { register } = service.setup(setupDeps); + register(Symbol(), createApp({ id: 'one_app' })); + register(Symbol(), createApp({ id: 'another_app', appRoute: '/custom/path' })); + + const start = await service.start(startDeps); + isAppRegistered = start.isAppRegistered; + }); + + it('returns false for unregistered apps', () => { + expect(isAppRegistered('oneApp')).toEqual(false); + }); + + it('returns true for registered apps', () => { + expect(isAppRegistered('another_app')).toEqual(true); + }); + }); + describe('getUrlForApp', () => { it('creates URL for unregistered appId', async () => { service.setup(setupDeps); diff --git a/packages/core/application/core-application-browser-internal/src/application_service.tsx b/packages/core/application/core-application-browser-internal/src/application_service.tsx index 8c4bf16f6dff6..678b74f95265e 100644 --- a/packages/core/application/core-application-browser-internal/src/application_service.tsx +++ b/packages/core/application/core-application-browser-internal/src/application_service.tsx @@ -327,6 +327,9 @@ export class ApplicationService { takeUntil(this.stop$) ), history: this.history!, + isAppRegistered: (appId: string): boolean => { + return applications$.value.get(appId) !== undefined; + }, getUrlForApp: ( appId, { diff --git a/packages/core/application/core-application-browser-mocks/src/application_service.mock.ts b/packages/core/application/core-application-browser-mocks/src/application_service.mock.ts index fe50fe3733496..a0bc498de94c4 100644 --- a/packages/core/application/core-application-browser-mocks/src/application_service.mock.ts +++ b/packages/core/application/core-application-browser-mocks/src/application_service.mock.ts @@ -51,6 +51,7 @@ const createStartContractMock = (): jest.Mocked => { navigateToApp: jest.fn(), navigateToUrl: jest.fn(), getUrlForApp: jest.fn(), + isAppRegistered: jest.fn(), }; }; @@ -92,6 +93,7 @@ const createInternalStartContractMock = ( currentActionMenu$: new BehaviorSubject(undefined), getComponent: jest.fn(), getUrlForApp: jest.fn(), + isAppRegistered: jest.fn(), navigateToApp: jest.fn().mockImplementation((appId) => currentAppId$.next(appId)), navigateToUrl: jest.fn(), history: createHistoryMock(), diff --git a/packages/core/application/core-application-browser/src/contracts.ts b/packages/core/application/core-application-browser/src/contracts.ts index 9f10bb1206c97..e8b2cd77028f6 100644 --- a/packages/core/application/core-application-browser/src/contracts.ts +++ b/packages/core/application/core-application-browser/src/contracts.ts @@ -68,9 +68,12 @@ export interface ApplicationStart { applications$: Observable>; /** - * Navigate to a given app + * Navigate to a given app. + * If a plugin is disabled any applications it registers won't be available either. + * Before rendering a UI element that a user could use to navigate to another application, + * first check if the destination application is actually available using the isAppRegistered API. * - * @param appId + * @param appId - The identifier of the app to navigate to * @param options - navigation options */ navigateToApp(appId: string, options?: NavigateToAppOptions): Promise; @@ -114,6 +117,14 @@ export interface ApplicationStart { */ navigateToUrl(url: string, options?: NavigateToUrlOptions): Promise; + /** + * Checks whether a given application is registered. + * + * @param appId - The identifier of the app to check + * @returns true if the given appId is registered in the system, false otherwise. + */ + isAppRegistered(appId: string): boolean; + /** * Returns the absolute path (or URL) to a given app, including the global base path. * diff --git a/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts index b78e5cec0b276..cdd00d9996a40 100644 --- a/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts @@ -143,6 +143,7 @@ export function createPluginStartContext< navigateToApp: deps.application.navigateToApp, navigateToUrl: deps.application.navigateToUrl, getUrlForApp: deps.application.getUrlForApp, + isAppRegistered: deps.application.isAppRegistered, currentLocation$: deps.application.currentLocation$, }, customBranding: deps.customBranding, diff --git a/src/plugins/discover/public/__mocks__/start_contract.ts b/src/plugins/discover/public/__mocks__/start_contract.ts index a9436797d616d..b7af207b2d20c 100644 --- a/src/plugins/discover/public/__mocks__/start_contract.ts +++ b/src/plugins/discover/public/__mocks__/start_contract.ts @@ -32,6 +32,7 @@ export const createStartContractMock = (): jest.Mocked => { capabilities, navigateToApp: jest.fn(), navigateToUrl: jest.fn(), + isAppRegistered: jest.fn(), getUrlForApp: jest.fn(), }; }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index c0035886e6b99..8b0a6f76f4624 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -156,6 +156,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` }, }, "getUrlForApp": [MockFunction], + "isAppRegistered": [MockFunction], "navigateToApp": [MockFunction], "navigateToUrl": [MockFunction], }, diff --git a/x-pack/plugins/enterprise_search/kibana.jsonc b/x-pack/plugins/enterprise_search/kibana.jsonc index 14a36c85c6c87..e284ae1862144 100644 --- a/x-pack/plugins/enterprise_search/kibana.jsonc +++ b/x-pack/plugins/enterprise_search/kibana.jsonc @@ -2,9 +2,11 @@ "type": "plugin", "id": "@kbn/enterprise-search-plugin", "owner": "@elastic/search-kibana", - // Could be categorised as Search in the future, but it currently needs to run in Observability too - "group": "platform", - "visibility": "shared", + // TODO this is currently used from Observability too, must be refactored before solution-specific builds + // see x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/search_connector_tab.tsx + // cc sphilipse + "group": "search", + "visibility": "private", "description": "Adds dashboards for discovering and managing Enterprise Search products.", "plugin": { "id": "enterpriseSearch", diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 06f14ba3d7037..1eb3384d4f9e3 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -80,8 +80,8 @@ export type EnterpriseSearchPublicStart = ReturnType { navigateToApp: async (app: string) => { action(`Navigate to: ${app}`); }, + isAppRegistered: (appId: string) => true, getUrlForApp: (url: string) => url, capabilities: { catalogue: {}, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc index 6699252ed69ed..cda6fdf0192fa 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc @@ -21,7 +21,6 @@ "optionalPlugins": [ "home", "serverless", - "enterpriseSearch" ], "requiredBundles": [ "kibanaReact", diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts index 88d007045052e..b7c6bb089663a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts @@ -6,11 +6,10 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; -import { ManagementSetup } from '@kbn/management-plugin/public'; -import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; -import { ServerlessPluginStart } from '@kbn/serverless/public'; -import { EnterpriseSearchPublicStart } from '@kbn/enterprise-search-plugin/public'; +import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import type { ManagementSetup } from '@kbn/management-plugin/public'; +import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { ObservabilityAIAssistantPublicSetup, @@ -32,7 +31,6 @@ export interface SetupDependencies { export interface StartDependencies { observabilityAIAssistant: ObservabilityAIAssistantPublicStart; serverless?: ServerlessPluginStart; - enterpriseSearch?: EnterpriseSearchPublicStart; } export interface ConfigSchema { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/search_connector_tab.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/search_connector_tab.tsx index d8f6032985f4b..336176d40ab33 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/search_connector_tab.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/search_connector_tab.tsx @@ -16,7 +16,7 @@ export const SELECTED_CONNECTOR_LOCAL_STORAGE_KEY = export function SearchConnectorTab() { const { application } = useKibana().services; - const url = application.getUrlForApp('enterprise_search', { path: '/content/connectors' }); + const url = application.getUrlForApp('enterpriseSearch', { path: '/content/connectors' }); return ( <> diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx index 57a167b1080fa..f9b750fcdc294 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_page.tsx @@ -22,9 +22,8 @@ export function SettingsPage() { const { setBreadcrumbs } = useAppContext(); const { services: { - application: { navigateToApp }, + application: { navigateToApp, isAppRegistered }, serverless, - enterpriseSearch, }, } = useKibana(); @@ -98,7 +97,7 @@ export function SettingsPage() { } ), content: , - disabled: enterpriseSearch == null, + disabled: !isAppRegistered('enterpriseSearch'), }, ]; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json index f0ad230f6f1b3..bc5cf69357dce 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json @@ -19,7 +19,6 @@ "@kbn/core-chrome-browser", "@kbn/observability-ai-assistant-plugin", "@kbn/serverless", - "@kbn/enterprise-search-plugin", "@kbn/management-settings-components-field-row", "@kbn/observability-shared-plugin", "@kbn/config-schema", diff --git a/x-pack/plugins/search_inference_endpoints/kibana.jsonc b/x-pack/plugins/search_inference_endpoints/kibana.jsonc index 7531316cd12e6..f535a9df27e99 100644 --- a/x-pack/plugins/search_inference_endpoints/kibana.jsonc +++ b/x-pack/plugins/search_inference_endpoints/kibana.jsonc @@ -2,8 +2,11 @@ "type": "plugin", "id": "@kbn/search-inference-endpoints", "owner": "@elastic/search-kibana", - "group": "platform", - "visibility": "shared", + // TODO enterpriseSearch depends on it, and Observability has a menu entry for enterpriseSearch + // must be refactored / fixed before solution-specific builds + // cc sphilipse + "group": "search", + "visibility": "private", "plugin": { "id": "searchInferenceEndpoints", "server": true, diff --git a/x-pack/plugins/search_playground/kibana.jsonc b/x-pack/plugins/search_playground/kibana.jsonc index 8b99add8587fa..37562347e9f37 100644 --- a/x-pack/plugins/search_playground/kibana.jsonc +++ b/x-pack/plugins/search_playground/kibana.jsonc @@ -2,9 +2,10 @@ "type": "plugin", "id": "@kbn/search-playground", "owner": "@elastic/search-kibana", - // @kbn/enterprise-search-plugin (platform) and @kbn/serverless-search (search) depend on it - "group": "platform", - "visibility": "shared", + // TODO @kbn/enterprise-search-plugin (platform) and @kbn/serverless-search (search) depend on it + // cc sphilipse + "group": "search", + "visibility": "private", "plugin": { "id": "searchPlayground", "server": true, diff --git a/x-pack/plugins/serverless_search/kibana.jsonc b/x-pack/plugins/serverless_search/kibana.jsonc index cae0a693846f1..f7b404edb37b1 100644 --- a/x-pack/plugins/serverless_search/kibana.jsonc +++ b/x-pack/plugins/serverless_search/kibana.jsonc @@ -35,7 +35,6 @@ "indexManagement", "searchConnectors", "searchInferenceEndpoints", - "searchPlayground", "usageCollection" ], "requiredBundles": ["kibanaReact"] diff --git a/x-pack/plugins/serverless_search/public/navigation_tree.ts b/x-pack/plugins/serverless_search/public/navigation_tree.ts index 066ab8e8c093e..ae8f41b8b17f8 100644 --- a/x-pack/plugins/serverless_search/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_search/public/navigation_tree.ts @@ -5,172 +5,179 @@ * 2.0. */ -import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser'; +import type { AppDeepLinkId, NavigationTreeDefinition } from '@kbn/core-chrome-browser'; +import type { ApplicationStart } from '@kbn/core-application-browser'; import { i18n } from '@kbn/i18n'; import { CONNECTORS_LABEL } from '../common/i18n_string'; -export const navigationTree = (): NavigationTreeDefinition => ({ - body: [ - { - type: 'navGroup', - id: 'search_project_nav', - title: 'Elasticsearch', - icon: 'logoElasticsearch', - defaultIsCollapsed: false, - isCollapsible: false, - breadcrumbStatus: 'hidden', - children: [ - { - id: 'data', - title: i18n.translate('xpack.serverlessSearch.nav.data', { - defaultMessage: 'Data', - }), - spaceBefore: 'm', - children: [ - { - title: i18n.translate('xpack.serverlessSearch.nav.content.indices', { - defaultMessage: 'Index Management', - }), - link: 'management:index_management', - breadcrumbStatus: - 'hidden' /* management sub-pages set their breadcrumbs themselves */, - getIsActive: ({ pathNameSerialized, prepend }) => { - return ( - pathNameSerialized.startsWith( - prepend('/app/management/data/index_management/') - ) || - pathNameSerialized.startsWith(prepend('/app/elasticsearch/indices')) || - pathNameSerialized.startsWith(prepend('/app/elasticsearch/start')) - ); +export const navigationTree = ({ isAppRegistered }: ApplicationStart): NavigationTreeDefinition => { + function isAvailable(appId: string, content: T): T[] { + return isAppRegistered(appId) ? [content] : []; + } + + return { + body: [ + { + type: 'navGroup', + id: 'search_project_nav', + title: 'Elasticsearch', + icon: 'logoElasticsearch', + defaultIsCollapsed: false, + isCollapsible: false, + breadcrumbStatus: 'hidden', + children: [ + { + id: 'data', + title: i18n.translate('xpack.serverlessSearch.nav.data', { + defaultMessage: 'Data', + }), + spaceBefore: 'm', + children: [ + { + title: i18n.translate('xpack.serverlessSearch.nav.content.indices', { + defaultMessage: 'Index Management', + }), + link: 'management:index_management', + breadcrumbStatus: + 'hidden' /* management sub-pages set their breadcrumbs themselves */, + getIsActive: ({ pathNameSerialized, prepend }) => { + return ( + pathNameSerialized.startsWith( + prepend('/app/management/data/index_management/') + ) || + pathNameSerialized.startsWith(prepend('/app/elasticsearch/indices')) || + pathNameSerialized.startsWith(prepend('/app/elasticsearch/start')) + ); + }, }, - }, - { - title: CONNECTORS_LABEL, - link: 'serverlessConnectors', - }, - ], - }, - { - id: 'build', - title: i18n.translate('xpack.serverlessSearch.nav.build', { - defaultMessage: 'Build', - }), - spaceBefore: 'm', - children: [ - { - id: 'dev_tools', - title: i18n.translate('xpack.serverlessSearch.nav.devTools', { - defaultMessage: 'Dev Tools', - }), - link: 'dev_tools', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/dev_tools')); + { + title: CONNECTORS_LABEL, + link: 'serverlessConnectors', }, - }, - { - id: 'searchPlayground', - title: i18n.translate('xpack.serverlessSearch.nav.build.searchPlayground', { - defaultMessage: 'Playground', + ], + }, + { + id: 'build', + title: i18n.translate('xpack.serverlessSearch.nav.build', { + defaultMessage: 'Build', + }), + spaceBefore: 'm', + children: [ + { + id: 'dev_tools', + title: i18n.translate('xpack.serverlessSearch.nav.devTools', { + defaultMessage: 'Dev Tools', + }), + link: 'dev_tools', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dev_tools')); + }, + }, + ...isAvailable('searchPlayground', { + id: 'searchPlayground', + title: i18n.translate('xpack.serverlessSearch.nav.build.searchPlayground', { + defaultMessage: 'Playground', + }), + link: 'searchPlayground' as AppDeepLinkId, }), - link: 'searchPlayground', - }, - ], - }, - { - id: 'relevance', - title: i18n.translate('xpack.serverlessSearch.nav.relevance', { - defaultMessage: 'Relevance', - }), - spaceBefore: 'm', - children: [ - { - id: 'searchInferenceEndpoints', - title: i18n.translate( - 'xpack.serverlessSearch.nav.relevance.searchInferenceEndpoints', - { - defaultMessage: 'Inference Endpoints', - } - ), - link: 'searchInferenceEndpoints', - }, - ], - }, - { - id: 'analyze', - title: i18n.translate('xpack.serverlessSearch.nav.analyze', { - defaultMessage: 'Analyze', - }), - spaceBefore: 'm', - children: [ - { - link: 'discover', - }, - { - link: 'dashboards', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/dashboards')); + ], + }, + { + id: 'relevance', + title: i18n.translate('xpack.serverlessSearch.nav.relevance', { + defaultMessage: 'Relevance', + }), + spaceBefore: 'm', + children: [ + { + id: 'searchInferenceEndpoints', + title: i18n.translate( + 'xpack.serverlessSearch.nav.relevance.searchInferenceEndpoints', + { + defaultMessage: 'Inference Endpoints', + } + ), + link: 'searchInferenceEndpoints', + }, + ], + }, + { + id: 'analyze', + title: i18n.translate('xpack.serverlessSearch.nav.analyze', { + defaultMessage: 'Analyze', + }), + spaceBefore: 'm', + children: [ + { + link: 'discover', + }, + { + link: 'dashboards', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dashboards')); + }, }, - }, - ], - }, - { - id: 'otherTools', - title: i18n.translate('xpack.serverlessSearch.nav.otherTools', { - defaultMessage: 'Other tools', - }), - spaceBefore: 'm', - children: [{ link: 'maps' }], - }, - ], - }, - ], - footer: [ - { - id: 'gettingStarted', - type: 'navItem', - title: i18n.translate('xpack.serverlessSearch.nav.gettingStarted', { - defaultMessage: 'Getting Started', - }), - link: 'serverlessElasticsearch', - icon: 'launch', - }, - { - type: 'navGroup', - id: 'project_settings_project_nav', - title: i18n.translate('xpack.serverlessSearch.nav.projectSettings', { - defaultMessage: 'Project settings', - }), - icon: 'gear', - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:modelManagement', - title: i18n.translate('xpack.serverlessSearch.nav.trainedModels', { - defaultMessage: 'Trained models', - }), - }, - { - link: 'management', - title: i18n.translate('xpack.serverlessSearch.nav.mngt', { - defaultMessage: 'Management', - }), - }, - { - id: 'cloudLinkUserAndRoles', - cloudLink: 'userAndRoles', - }, - { - id: 'cloudLinkDeployment', - cloudLink: 'deployment', - title: i18n.translate('xpack.serverlessSearch.nav.performance', { - defaultMessage: 'Performance', - }), - }, - { - id: 'cloudLinkBilling', - cloudLink: 'billingAndSub', - }, - ], - }, - ], -}); + ], + }, + { + id: 'otherTools', + title: i18n.translate('xpack.serverlessSearch.nav.otherTools', { + defaultMessage: 'Other tools', + }), + spaceBefore: 'm', + children: [{ link: 'maps' }], + }, + ], + }, + ], + footer: [ + { + id: 'gettingStarted', + type: 'navItem', + title: i18n.translate('xpack.serverlessSearch.nav.gettingStarted', { + defaultMessage: 'Getting Started', + }), + link: 'serverlessElasticsearch', + icon: 'launch', + }, + { + type: 'navGroup', + id: 'project_settings_project_nav', + title: i18n.translate('xpack.serverlessSearch.nav.projectSettings', { + defaultMessage: 'Project settings', + }), + icon: 'gear', + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:modelManagement', + title: i18n.translate('xpack.serverlessSearch.nav.trainedModels', { + defaultMessage: 'Trained models', + }), + }, + { + link: 'management', + title: i18n.translate('xpack.serverlessSearch.nav.mngt', { + defaultMessage: 'Management', + }), + }, + { + id: 'cloudLinkUserAndRoles', + cloudLink: 'userAndRoles', + }, + { + id: 'cloudLinkDeployment', + cloudLink: 'deployment', + title: i18n.translate('xpack.serverlessSearch.nav.performance', { + defaultMessage: 'Performance', + }), + }, + { + id: 'cloudLinkBilling', + cloudLink: 'billingAndSub', + }, + ], + }, + ], + }; +}; diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts index 3d246e4be2929..d097cd1eb3ad4 100644 --- a/x-pack/plugins/serverless_search/public/plugin.ts +++ b/x-pack/plugins/serverless_search/public/plugin.ts @@ -148,7 +148,7 @@ export class ServerlessSearchPlugin const { serverless, management, indexManagement, security } = services; serverless.setProjectHome(services.searchIndices.startRoute); - const navigationTree$ = of(navigationTree()); + const navigationTree$ = of(navigationTree(core.application)); serverless.initNavigation('es', navigationTree$, { dataTestSubj: 'svlSearchSideNav' }); const extendCardNavDefinitions = serverless.getNavigationCards( diff --git a/x-pack/plugins/serverless_search/public/types.ts b/x-pack/plugins/serverless_search/public/types.ts index 19b3f0fa6baa5..b9c267874b3d9 100644 --- a/x-pack/plugins/serverless_search/public/types.ts +++ b/x-pack/plugins/serverless_search/public/types.ts @@ -8,7 +8,6 @@ import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; -import type { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; @@ -37,7 +36,6 @@ export interface ServerlessSearchPluginSetupDependencies { export interface ServerlessSearchPluginStartDependencies { cloud: CloudStart; console: ConsolePluginStart; - searchPlayground: SearchPlaygroundPluginStart; searchInferenceEndpoints?: SearchInferenceEndpointsPluginStart; management: ManagementStart; security: SecurityPluginStart; diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json index 0f7a803a68f7d..794f146299a0f 100644 --- a/x-pack/plugins/serverless_search/tsconfig.json +++ b/x-pack/plugins/serverless_search/tsconfig.json @@ -47,7 +47,6 @@ "@kbn/search-connectors-plugin", "@kbn/index-management-shared-types", "@kbn/react-kibana-context-render", - "@kbn/search-playground", "@kbn/security-api-key-management", "@kbn/search-inference-endpoints", "@kbn/security-plugin-types-common", @@ -55,5 +54,6 @@ "@kbn/core-http-server", "@kbn/logging", "@kbn/security-plugin-types-public", + "@kbn/core-application-browser", ] } diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/context/application.tsx b/x-pack/plugins/triggers_actions_ui/.storybook/context/application.tsx index c05ca6a920cb8..313c7c8e3b1c6 100644 --- a/x-pack/plugins/triggers_actions_ui/.storybook/context/application.tsx +++ b/x-pack/plugins/triggers_actions_ui/.storybook/context/application.tsx @@ -24,6 +24,7 @@ export const getDefaultServicesApplication = ( navigateToApp: async (app: string) => { action(`Navigate to: ${app}`); }, + isAppRegistered: (appId: string) => true, getUrlForApp: (url: string) => url, capabilities: getDefaultCapabilities(), applications$: of(applications), From 671ff30516f2ca302962efffae7585dcd7ddfce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 14 Nov 2024 13:30:13 +0100 Subject: [PATCH 06/23] [AI Assistant] Use `semantic_text` for internal knowledge base (#186499) Closes https://github.com/elastic/obs-ai-assistant-team/issues/162 Closes https://github.com/elastic/kibana/issues/192757 This replaces the ML inference pipeline with `semantic_text` and adds a migration task that runs automatically when Kibana starts. Blocked by: - https://github.com/elastic/elasticsearch/pull/110027 - https://github.com/elastic/elasticsearch/pull/110033 - https://github.com/elastic/ml-team/issues/1298 --- ...ssage_knowledge_base_setup_error_panel.tsx | 36 +- .../src/hooks/use_knowledge_base.tsx | 14 +- .../server/config.ts | 2 +- .../server/functions/context.ts | 6 +- .../server/functions/index.ts | 8 +- .../server/plugin.ts | 15 +- .../server/routes/knowledge_base/route.ts | 86 +++-- .../server/routes/register_routes.ts | 3 +- .../server/routes/types.ts | 1 + .../server/service/client/index.ts | 20 +- .../server/service/index.ts | 84 ++--- .../server/service/inference_endpoint.ts | 104 ++++++ .../server/service/kb_component_template.ts | 9 + .../service/knowledge_base_service/index.ts | 314 +++++++----------- ...ter_migrate_knowledge_base_entries_task.ts | 149 +++++++++ .../translations/translations/fr-FR.json | 16 +- .../translations/translations/ja-JP.json | 16 +- .../translations/translations/zh-CN.json | 16 +- .../knowledge_base_8_15/data.json.gz | Bin 0 -> 3099 bytes .../knowledge_base_8_15/mappings.json | 102 ++++++ .../configs/index.ts | 2 +- .../complete/functions/summarize.spec.ts | 10 +- .../tests/knowledge_base/helpers.ts | 17 + .../knowledge_base/knowledge_base.spec.ts | 18 +- .../knowledge_base_migration.spec.ts | 160 +++++++++ .../knowledge_base_setup.spec.ts | 44 ++- .../knowledge_base_status.spec.ts | 56 +++- .../knowledge_base_user_instructions.spec.ts | 13 +- .../knowledge_base_management/index.spec.ts | 18 +- .../check_registered_task_types.ts | 1 + .../observability/ai_assistant/config.ts | 2 +- .../tests/knowledge_base/helpers.ts | 20 -- .../knowledge_base/knowledge_base.spec.ts | 5 +- .../knowledge_base_setup.spec.ts | 45 ++- .../knowledge_base_status.spec.ts | 30 +- .../knowledge_base_user_instructions.spec.ts | 4 +- 36 files changed, 1012 insertions(+), 434 deletions(-) create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/task_manager_definitions/register_migrate_knowledge_base_entries_task.ts create mode 100644 x-pack/test/functional/es_archives/observability/ai_assistant/knowledge_base_8_15/data.json.gz create mode 100644 x-pack/test/functional/es_archives/observability/ai_assistant/knowledge_base_8_15/mappings.json create mode 100644 x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_migration.spec.ts delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/helpers.ts diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx index eeff9c8afd7f3..f7267d6d21855 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx @@ -37,7 +37,9 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ }) { const { http } = useKibana().services; - const modelName = knowledgeBase.status.value?.model_name; + const modelId = knowledgeBase.status.value?.endpoint?.service_settings?.model_id; + const deploymentState = knowledgeBase.status.value?.model_stats?.deployment_state; + const allocationState = knowledgeBase.status.value?.model_stats?.allocation_state; return (
    - {!knowledgeBase.status.value?.deployment_state ? ( + {!deploymentState ? (
  • {' '} {modelName}, + modelId: {modelId}, }} />
  • ) : null} - {knowledgeBase.status.value?.deployment_state && - knowledgeBase.status.value.deployment_state !== 'started' ? ( + {deploymentState && deploymentState !== 'started' ? (
  • {' '} {modelName}, - deploymentState: ( - {knowledgeBase.status.value?.deployment_state} - ), + modelId: {modelId}, + deploymentState: {deploymentState}, }} />
  • ) : null} - {knowledgeBase.status.value?.allocation_state && - knowledgeBase.status.value.allocation_state !== 'fully_allocated' ? ( + {allocationState && allocationState !== 'fully_allocated' ? (
  • {' '} {modelName}, - allocationState: ( - {knowledgeBase.status.value?.allocation_state} - ), + modelId: {modelId}, + allocationState: {allocationState}, }} />
  • @@ -114,9 +110,9 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ ; + status: AbortableAsyncState>; isInstalling: boolean; installError?: Error; install: () => Promise; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts index 4d0b9fef3f2f4..4df8891bd06fc 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts @@ -9,7 +9,7 @@ import { schema, type TypeOf } from '@kbn/config-schema'; export const config = schema.object({ enabled: schema.boolean({ defaultValue: true }), - modelId: schema.maybe(schema.string()), + modelId: schema.maybe(schema.string()), // TODO: Remove scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])), enableKnowledgeBase: schema.boolean({ defaultValue: true }), }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts index fd57968617187..80ddf3cbc0a0d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts @@ -24,8 +24,8 @@ export function registerContextFunction({ client, functions, resources, - isKnowledgeBaseAvailable, -}: FunctionRegistrationParameters & { isKnowledgeBaseAvailable: boolean }) { + isKnowledgeBaseReady, +}: FunctionRegistrationParameters & { isKnowledgeBaseReady: boolean }) { functions.registerFunction( { name: CONTEXT_FUNCTION_NAME, @@ -54,7 +54,7 @@ export function registerContextFunction({ ...(dataWithinTokenLimit.length ? { data_on_screen: dataWithinTokenLimit } : {}), }; - if (!isKnowledgeBaseAvailable) { + if (!isKnowledgeBaseReady) { return { content }; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/index.ts index 0313d29d3b209..ce4c8e59fbae2 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/index.ts @@ -87,7 +87,7 @@ export const registerFunctions: RegistrationCallback = async ({ ); } - const { ready: isReady } = await client.getKnowledgeBaseStatus(); + const { ready: isKnowledgeBaseReady } = await client.getKnowledgeBaseStatus(); functions.registerInstruction(({ availableFunctionNames }) => { const instructions: string[] = []; @@ -109,7 +109,7 @@ export const registerFunctions: RegistrationCallback = async ({ Data that is compact enough automatically gets included in the response for the "${CONTEXT_FUNCTION_NAME}" function.`); } - if (isReady) { + if (isKnowledgeBaseReady) { if (availableFunctionNames.includes(SUMMARIZE_FUNCTION_NAME)) { instructions.push(`You can use the "${SUMMARIZE_FUNCTION_NAME}" function to store new information you have learned in a knowledge database. Only use this function when the user asks for it. @@ -129,11 +129,11 @@ export const registerFunctions: RegistrationCallback = async ({ return instructions.map((instruction) => dedent(instruction)); }); - if (isReady) { + if (isKnowledgeBaseReady) { registerSummarizationFunction(registrationParameters); } - registerContextFunction({ ...registrationParameters, isKnowledgeBaseAvailable: isReady }); + registerContextFunction({ ...registrationParameters, isKnowledgeBaseReady }); registerElasticsearchFunction(registrationParameters); const request = registrationParameters.resources.request; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index 3bdff9eb17606..98a6232563054 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -36,6 +36,7 @@ import { registerFunctions } from './functions'; import { recallRankingEvent } from './analytics/recall_ranking'; import { initLangtrace } from './service/client/instrumentation/init_langtrace'; import { aiAssistantCapabilities } from '../common/capabilities'; +import { registerMigrateKnowledgeBaseEntriesTask } from './service/task_manager_definitions/register_migrate_knowledge_base_entries_task'; export class ObservabilityAIAssistantPlugin implements @@ -114,7 +115,8 @@ export class ObservabilityAIAssistantPlugin }) as ObservabilityAIAssistantRouteHandlerResources['plugins']; // Using once to make sure the same model ID is used during service init and Knowledge base setup - const getModelId = once(async () => { + const getSearchConnectorModelId = once(async () => { + // TODO: Remove this once the modelId is removed from the config const configModelId = this.config.modelId; if (configModelId) { return configModelId; @@ -156,11 +158,18 @@ export class ObservabilityAIAssistantPlugin const service = (this.service = new ObservabilityAIAssistantService({ logger: this.logger.get('service'), core, - taskManager: plugins.taskManager, - getModelId, + getSearchConnectorModelId, enableKnowledgeBase: this.config.enableKnowledgeBase, })); + registerMigrateKnowledgeBaseEntriesTask({ + core, + taskManager: plugins.taskManager, + logger: this.logger, + }).catch((error) => { + this.logger.error(`Failed to register migrate knowledge base entries task: ${error}`); + }); + service.register(registerFunctions); registerServerRoutes({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts index 0f1852c0e396c..50ce85e3578e9 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts @@ -5,14 +5,16 @@ * 2.0. */ -import type { - MlDeploymentAllocationState, - MlDeploymentState, -} from '@elastic/elasticsearch/lib/api/types'; import pLimit from 'p-limit'; import { notImplemented } from '@hapi/boom'; import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; +import { + InferenceInferenceEndpointInfo, + MlDeploymentAllocationState, + MlDeploymentState, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import moment from 'moment'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { Instruction, KnowledgeBaseEntry, KnowledgeBaseEntryRole } from '../../../common/types'; @@ -21,44 +23,86 @@ const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({ options: { tags: ['access:ai_assistant'], }, - handler: async ( - resources - ): Promise<{ - enabled: boolean; + handler: async ({ + service, + request, + }): Promise<{ + errorMessage?: string; ready: boolean; - error?: any; - deployment_state?: MlDeploymentState; - allocation_state?: MlDeploymentAllocationState; - model_name?: string; + enabled: boolean; + endpoint?: Partial; + model_stats?: { + deployment_state: MlDeploymentState | undefined; + allocation_state: MlDeploymentAllocationState | undefined; + }; }> => { - const client = await resources.service.getClient({ request: resources.request }); + const client = await service.getClient({ request }); if (!client) { throw notImplemented(); } - return await client.getKnowledgeBaseStatus(); + return client.getKnowledgeBaseStatus(); }, }); const setupKnowledgeBase = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: t.partial({ + query: t.partial({ + model_id: t.string, + }), + }), options: { tags: ['access:ai_assistant'], timeout: { - idleSocket: 20 * 60 * 1000, // 20 minutes + idleSocket: moment.duration(20, 'minutes').asMilliseconds(), }, }, - handler: async (resources): Promise<{}> => { + handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); if (!client) { throw notImplemented(); } - await client.setupKnowledgeBase(); + const { model_id: modelId } = resources.params?.query ?? {}; + + return await client.setupKnowledgeBase(modelId); + }, +}); + +const resetKnowledgeBase = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/kb/reset', + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise<{ result: string }> => { + const client = await resources.service.getClient({ request: resources.request }); + + if (!client) { + throw notImplemented(); + } + + await client.resetKnowledgeBase(); + + return { result: 'success' }; + }, +}); + +const semanticTextMigrationKnowledgeBase = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/kb/semantic_text_migration', + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise => { + const client = await resources.service.getClient({ request: resources.request }); + + if (!client) { + throw notImplemented(); + } - return {}; + return client.migrateKnowledgeBaseToSemanticText(); }, }); @@ -225,8 +269,8 @@ const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ throw notImplemented(); } - const status = await client.getKnowledgeBaseStatus(); - if (!status.ready) { + const { ready } = await client.getKnowledgeBaseStatus(); + if (!ready) { throw new Error('Knowledge base is not ready'); } @@ -252,7 +296,9 @@ const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ }); export const knowledgeBaseRoutes = { + ...semanticTextMigrationKnowledgeBase, ...setupKnowledgeBase, + ...resetKnowledgeBase, ...getKnowledgeBaseStatus, ...getKnowledgeBaseEntries, ...saveKnowledgeBaseUserInstruction, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts index cd1bccdda6734..1a6140968c925 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/register_routes.ts @@ -9,13 +9,14 @@ import type { Logger } from '@kbn/logging'; import { registerRoutes } from '@kbn/server-route-repository'; import { getGlobalObservabilityAIAssistantServerRouteRepository } from './get_global_observability_ai_assistant_route_repository'; import type { ObservabilityAIAssistantRouteHandlerResources } from './types'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../types'; export function registerServerRoutes({ core, logger, dependencies, }: { - core: CoreSetup; + core: CoreSetup; logger: Logger; dependencies: Omit< ObservabilityAIAssistantRouteHandlerResources, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts index c0b37b7142a83..b817328d22c64 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts @@ -68,6 +68,7 @@ export interface ObservabilityAIAssistantRouteHandlerResources { export interface ObservabilityAIAssistantRouteCreateOptions { options: { timeout?: { + payload?: number; idleSocket?: number; }; tags: Array<'access:ai_assistant'>; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts index 048bbd2d362c2..2bd2fdcf22462 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts @@ -80,6 +80,7 @@ import { LangtraceServiceProvider, withLangtraceChatCompleteSpan, } from './operators/with_langtrace_chat_complete_span'; +import { runSemanticTextKnowledgeBaseMigration } from '../task_manager_definitions/register_migrate_knowledge_base_entries_task'; const MAX_FUNCTION_CALLS = 8; @@ -721,11 +722,24 @@ export class ObservabilityAIAssistantClient { }; getKnowledgeBaseStatus = () => { - return this.dependencies.knowledgeBaseService.status(); + return this.dependencies.knowledgeBaseService.getStatus(); }; - setupKnowledgeBase = () => { - return this.dependencies.knowledgeBaseService.setup(); + setupKnowledgeBase = (modelId: string | undefined) => { + const { esClient } = this.dependencies; + return this.dependencies.knowledgeBaseService.setup(esClient, modelId); + }; + + resetKnowledgeBase = () => { + const { esClient } = this.dependencies; + return this.dependencies.knowledgeBaseService.reset(esClient); + }; + + migrateKnowledgeBaseToSemanticText = () => { + return runSemanticTextKnowledgeBaseMigration({ + esClient: this.dependencies.esClient, + logger: this.dependencies.logger, + }); }; addUserInstruction = async ({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts index eb7eab19340ce..6dcfbf1796501 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts @@ -10,7 +10,6 @@ import { createConcreteWriteIndex, getDataStreamAdapter } from '@kbn/alerting-pl import type { CoreSetup, CoreStart, KibanaRequest, Logger } from '@kbn/core/server'; import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; -import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { once } from 'lodash'; import type { AssistantScope } from '@kbn/ai-assistant-common'; import { ObservabilityAIAssistantScreenContextRequest } from '../../common/types'; @@ -43,16 +42,13 @@ export const resourceNames = { conversations: getResourceName('index-template-conversations'), kb: getResourceName('index-template-kb'), }, - pipelines: { - kb: getResourceName('kb-ingest-pipeline'), - }, }; export class ObservabilityAIAssistantService { private readonly core: CoreSetup; private readonly logger: Logger; - private readonly getModelId: () => Promise; - public kbService?: KnowledgeBaseService; + private readonly getSearchConnectorModelId: () => Promise; + private kbService?: KnowledgeBaseService; private enableKnowledgeBase: boolean; private readonly registrations: RegistrationCallback[] = []; @@ -60,36 +56,28 @@ export class ObservabilityAIAssistantService { constructor({ logger, core, - taskManager, - getModelId, + getSearchConnectorModelId, enableKnowledgeBase, }: { logger: Logger; core: CoreSetup; - taskManager: TaskManagerSetupContract; - getModelId: () => Promise; + getSearchConnectorModelId: () => Promise; enableKnowledgeBase: boolean; }) { this.core = core; this.logger = logger; - this.getModelId = getModelId; + this.getSearchConnectorModelId = getSearchConnectorModelId; this.enableKnowledgeBase = enableKnowledgeBase; - this.allowInit(); - } - - getKnowledgeBaseStatus() { - return this.init().then(() => { - return this.kbService!.status(); - }); + this.resetInit(); } init = async () => {}; - private allowInit = () => { + private resetInit = () => { this.init = once(async () => { return this.doInit().catch((error) => { - this.allowInit(); + this.resetInit(); // reset the once flag if an error occurs throw error; }); }); @@ -97,18 +85,18 @@ export class ObservabilityAIAssistantService { private doInit = async () => { try { - const [coreStart, pluginsStart] = await this.core.getStartServices(); + this.logger.debug('Setting up index assets'); + const [coreStart] = await this.core.getStartServices(); - const elserModelId = await this.getModelId(); + const { asInternalUser } = coreStart.elasticsearch.client; - const esClient = coreStart.elasticsearch.client; - await esClient.asInternalUser.cluster.putComponentTemplate({ + await asInternalUser.cluster.putComponentTemplate({ create: false, name: resourceNames.componentTemplate.conversations, template: conversationComponentTemplate, }); - await esClient.asInternalUser.indices.putIndexTemplate({ + await asInternalUser.indices.putIndexTemplate({ name: resourceNames.indexTemplate.conversations, composed_of: [resourceNames.componentTemplate.conversations], create: false, @@ -119,18 +107,13 @@ export class ObservabilityAIAssistantService { auto_expand_replicas: '0-1', hidden: true, }, - mappings: { - _meta: { - model: elserModelId, - }, - }, }, }); const conversationAliasName = resourceNames.aliases.conversations; await createConcreteWriteIndex({ - esClient: esClient.asInternalUser, + esClient: asInternalUser, logger: this.logger, totalFieldsLimit: 10000, indexPatterns: { @@ -143,34 +126,15 @@ export class ObservabilityAIAssistantService { dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts: false }), }); - await esClient.asInternalUser.cluster.putComponentTemplate({ + // Knowledge base: component template + await asInternalUser.cluster.putComponentTemplate({ create: false, name: resourceNames.componentTemplate.kb, template: kbComponentTemplate, }); - await esClient.asInternalUser.ingest.putPipeline({ - id: resourceNames.pipelines.kb, - processors: [ - { - inference: { - model_id: elserModelId, - target_field: 'ml', - field_map: { - text: 'text_field', - }, - inference_config: { - // @ts-expect-error - text_expansion: { - results_field: 'tokens', - }, - }, - }, - }, - ], - }); - - await esClient.asInternalUser.indices.putIndexTemplate({ + // Knowledge base: index template + await asInternalUser.indices.putIndexTemplate({ name: resourceNames.indexTemplate.kb, composed_of: [resourceNames.componentTemplate.kb], create: false, @@ -186,8 +150,9 @@ export class ObservabilityAIAssistantService { const kbAliasName = resourceNames.aliases.kb; + // Knowledge base: write index await createConcreteWriteIndex({ - esClient: esClient.asInternalUser, + esClient: asInternalUser, logger: this.logger, totalFieldsLimit: 10000, indexPatterns: { @@ -202,15 +167,16 @@ export class ObservabilityAIAssistantService { this.kbService = new KnowledgeBaseService({ logger: this.logger.get('kb'), - esClient, - taskManagerStart: pluginsStart.taskManager, - getModelId: this.getModelId, + esClient: { + asInternalUser, + }, + getSearchConnectorModelId: this.getSearchConnectorModelId, enabled: this.enableKnowledgeBase, }); this.logger.info('Successfully set up index assets'); } catch (error) { - this.logger.error(`Failed to initialize service: ${error.message}`); + this.logger.error(`Failed setting up index assets: ${error.message}`); this.logger.debug(error); throw error; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts new file mode 100644 index 0000000000000..1d09311dbd6ea --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/inference_endpoint.ts @@ -0,0 +1,104 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { Logger } from '@kbn/logging'; +import moment from 'moment'; + +export const AI_ASSISTANT_KB_INFERENCE_ID = 'ai_assistant_kb_inference'; + +export async function createInferenceEndpoint({ + esClient, + logger, + modelId = '.elser_model_2', +}: { + esClient: { + asCurrentUser: ElasticsearchClient; + }; + logger: Logger; + modelId: string | undefined; +}) { + try { + logger.debug(`Creating inference endpoint "${AI_ASSISTANT_KB_INFERENCE_ID}"`); + + return await esClient.asCurrentUser.inference.put( + { + inference_id: AI_ASSISTANT_KB_INFERENCE_ID, + task_type: 'sparse_embedding', + inference_config: { + service: 'elasticsearch', + service_settings: { + model_id: modelId, + adaptive_allocations: { enabled: true }, + num_threads: 1, + }, + task_settings: {}, + }, + }, + { + requestTimeout: moment.duration(2, 'minutes').asMilliseconds(), + } + ); + } catch (e) { + logger.error( + `Failed to create inference endpoint "${AI_ASSISTANT_KB_INFERENCE_ID}": ${e.message}` + ); + throw e; + } +} + +export async function deleteInferenceEndpoint({ + esClient, + logger, +}: { + esClient: { + asCurrentUser: ElasticsearchClient; + }; + logger: Logger; +}) { + try { + const response = await esClient.asCurrentUser.inference.delete({ + inference_id: AI_ASSISTANT_KB_INFERENCE_ID, + force: true, + }); + + return response; + } catch (e) { + logger.error(`Failed to delete inference endpoint: ${e.message}`); + throw e; + } +} + +export async function getInferenceEndpoint({ + esClient, + logger, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; +}) { + try { + const response = await esClient.asInternalUser.inference.get({ + inference_id: AI_ASSISTANT_KB_INFERENCE_ID, + }); + + if (response.endpoints.length > 0) { + return response.endpoints[0]; + } + } catch (e) { + logger.error(`Failed to fetch inference endpoint: ${e.message}`); + throw e; + } +} + +export function isInferenceEndpointMissingOrUnavailable(error: Error) { + return ( + error instanceof errors.ResponseError && + (error.body?.error?.type === 'resource_not_found_exception' || + error.body?.error?.type === 'status_exception') + ); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts index b1b2d3293a234..6cf89b0c9e22d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/kb_component_template.ts @@ -6,6 +6,7 @@ */ import { ClusterComponentTemplate } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { AI_ASSISTANT_KB_INFERENCE_ID } from './inference_endpoint'; const keyword = { type: 'keyword' as const, @@ -58,6 +59,14 @@ export const kbComponentTemplate: ClusterComponentTemplate['component_template'] }, namespace: keyword, text, + semantic_text: { + type: 'semantic_text', + inference_id: AI_ASSISTANT_KB_INFERENCE_ID, + // @ts-expect-error: @elastic/elasticsearch does not have this type yet + model_settings: { + task_type: 'sparse_embedding', + }, + }, 'ml.tokens': { type: 'rank_features', }, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts index 92ce3a4a7e03b..66a49cdc29bee 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -4,15 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { errors } from '@elastic/elasticsearch'; -import { serverUnavailable, gatewayTimeout, badRequest } from '@hapi/boom'; + +import { serverUnavailable } from '@hapi/boom'; import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; -import pRetry from 'p-retry'; import { orderBy } from 'lodash'; import { encode } from 'gpt-tokenizer'; -import { MlTrainedModelDeploymentNodesStats } from '@elastic/elasticsearch/lib/api/types'; import { resourceNames } from '..'; import { Instruction, @@ -22,13 +19,21 @@ import { } from '../../../common/types'; import { getAccessQuery } from '../util/get_access_query'; import { getCategoryQuery } from '../util/get_category_query'; +import { + AI_ASSISTANT_KB_INFERENCE_ID, + createInferenceEndpoint, + deleteInferenceEndpoint, + getInferenceEndpoint, + isInferenceEndpointMissingOrUnavailable, +} from '../inference_endpoint'; import { recallFromConnectors } from './recall_from_connectors'; interface Dependencies { - esClient: { asInternalUser: ElasticsearchClient }; + esClient: { + asInternalUser: ElasticsearchClient; + }; logger: Logger; - taskManagerStart: TaskManagerStartContract; - getModelId: () => Promise; + getSearchConnectorModelId: () => Promise; enabled: boolean; } @@ -40,20 +45,6 @@ export interface RecalledEntry { labels?: Record; } -function isModelMissingOrUnavailableError(error: Error) { - return ( - error instanceof errors.ResponseError && - (error.body?.error?.type === 'resource_not_found_exception' || - error.body?.error?.type === 'status_exception') - ); -} -function isCreateModelValidationError(error: Error) { - return ( - error instanceof errors.ResponseError && - error.statusCode === 400 && - error.body?.error?.type === 'action_request_validation_exception' - ); -} function throwKnowledgeBaseNotReady(body: any) { throw serverUnavailable(`Knowledge base is not ready yet`, body); } @@ -61,202 +52,64 @@ function throwKnowledgeBaseNotReady(body: any) { export class KnowledgeBaseService { constructor(private readonly dependencies: Dependencies) {} - setup = async () => { - this.dependencies.logger.debug('Setting up knowledge base'); - if (!this.dependencies.enabled) { - return; - } - const elserModelId = await this.dependencies.getModelId(); - - const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 }; - const getModelInfo = async () => { - return await this.dependencies.esClient.asInternalUser.ml.getTrainedModels({ - model_id: elserModelId, - include: 'definition_status', - }); - }; - - const isModelInstalledAndReady = async () => { - try { - const getResponse = await getModelInfo(); - this.dependencies.logger.debug( - () => 'Model definition status:\n' + JSON.stringify(getResponse.trained_model_configs[0]) - ); - - return Boolean(getResponse.trained_model_configs[0]?.fully_defined); - } catch (error) { - if (isModelMissingOrUnavailableError(error)) { - return false; - } - - throw error; - } - }; - - const installModelIfDoesNotExist = async () => { - const modelInstalledAndReady = await isModelInstalledAndReady(); - if (!modelInstalledAndReady) { - await installModel(); - } - }; - - const installModel = async () => { - this.dependencies.logger.info(`Installing ${elserModelId} model`); - try { - await this.dependencies.esClient.asInternalUser.ml.putTrainedModel( - { - model_id: elserModelId, - input: { - field_names: ['text_field'], - }, - wait_for_completion: true, - }, - { requestTimeout: '20m' } - ); - } catch (error) { - if (isCreateModelValidationError(error)) { - throw badRequest(error); - } else { - throw error; - } - } - this.dependencies.logger.info(`Finished installing ${elserModelId} model`); - }; - - const pollForModelInstallCompleted = async () => { - await pRetry(async () => { - this.dependencies.logger.info(`Polling installation of ${elserModelId} model`); - const modelInstalledAndReady = await isModelInstalledAndReady(); - if (!modelInstalledAndReady) { - throwKnowledgeBaseNotReady({ - message: 'Model is not fully defined', - }); - } - }, retryOptions); - }; - await installModelIfDoesNotExist(); - await pollForModelInstallCompleted(); + async setup( + esClient: { + asCurrentUser: ElasticsearchClient; + asInternalUser: ElasticsearchClient; + }, + modelId: string | undefined + ) { + await deleteInferenceEndpoint({ esClient, logger: this.dependencies.logger }).catch((e) => {}); // ensure existing inference endpoint is deleted + return createInferenceEndpoint({ esClient, logger: this.dependencies.logger, modelId }); + } + async reset(esClient: { asCurrentUser: ElasticsearchClient }) { try { - await this.dependencies.esClient.asInternalUser.ml.startTrainedModelDeployment({ - model_id: elserModelId, - wait_for: 'fully_allocated', - }); + await deleteInferenceEndpoint({ esClient, logger: this.dependencies.logger }); } catch (error) { - this.dependencies.logger.debug(`Error starting ${elserModelId} model deployment`); - this.dependencies.logger.debug(error); - if (!isModelMissingOrUnavailableError(error)) { - throw error; - } - } - - await pRetry(async () => { - const response = await this.dependencies.esClient.asInternalUser.ml.getTrainedModelsStats({ - model_id: elserModelId, - }); - - const isReady = response.trained_model_stats.some((stats) => - (stats.deployment_stats?.nodes as unknown as MlTrainedModelDeploymentNodesStats[]).some( - (node) => node.routing_state.routing_state === 'started' - ) - ); - - if (isReady) { + if (isInferenceEndpointMissingOrUnavailable(error)) { return; } - - this.dependencies.logger.debug(`${elserModelId} model is not allocated yet`); - this.dependencies.logger.debug(() => JSON.stringify(response)); - - throw gatewayTimeout(); - }, retryOptions); - - this.dependencies.logger.info(`${elserModelId} model is ready`); - }; - - status = async () => { - this.dependencies.logger.debug('Checking model status'); - if (!this.dependencies.enabled) { - return { ready: false, enabled: false }; - } - const elserModelId = await this.dependencies.getModelId(); - - try { - const modelStats = await this.dependencies.esClient.asInternalUser.ml.getTrainedModelsStats({ - model_id: elserModelId, - }); - const elserModelStats = modelStats.trained_model_stats[0]; - const deploymentState = elserModelStats.deployment_stats?.state; - const allocationState = elserModelStats.deployment_stats?.allocation_status.state; - const ready = deploymentState === 'started' && allocationState === 'fully_allocated'; - - this.dependencies.logger.debug( - `Model deployment state: ${deploymentState}, allocation state: ${allocationState}, ready: ${ready}` - ); - - return { - ready, - deployment_state: deploymentState, - allocation_state: allocationState, - model_name: elserModelId, - enabled: true, - }; - } catch (error) { - this.dependencies.logger.debug( - `Failed to get status for model "${elserModelId}" due to ${error.message}` - ); - - return { - error: error instanceof errors.ResponseError ? error.body.error : String(error), - ready: false, - enabled: true, - model_name: elserModelId, - }; + throw error; } - }; + } private async recallFromKnowledgeBase({ queries, categories, namespace, user, - modelId, }: { queries: Array<{ text: string; boost?: number }>; categories?: string[]; namespace: string; user?: { name: string }; - modelId: string; }): Promise { - const esQuery = { - bool: { - should: queries.map(({ text, boost = 1 }) => ({ - text_expansion: { - 'ml.tokens': { - model_text: text, - model_id: modelId, - boost, - }, - }, - })), - filter: [ - ...getAccessQuery({ - user, - namespace, - }), - ...getCategoryQuery({ categories }), - - // exclude user instructions - { bool: { must_not: { term: { type: KnowledgeBaseType.UserInstruction } } } }, - ], - }, - }; - const response = await this.dependencies.esClient.asInternalUser.search< Pick & { doc_id?: string } >({ index: [resourceNames.aliases.kb], - query: esQuery, + query: { + bool: { + should: queries.map(({ text, boost = 1 }) => ({ + semantic: { + field: 'semantic_text', + query: text, + boost, + }, + })), + filter: [ + ...getAccessQuery({ + user, + namespace, + }), + ...getCategoryQuery({ categories }), + + // exclude user instructions + { bool: { must_not: { term: { type: KnowledgeBaseType.UserInstruction } } } }, + ], + }, + }, size: 20, _source: { includes: ['text', 'is_correction', 'labels', 'doc_id', 'title'], @@ -295,7 +148,7 @@ export class KnowledgeBaseService { this.dependencies.logger.debug( () => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"` ); - const modelId = await this.dependencies.getModelId(); + const modelId = await this.dependencies.getSearchConnectorModelId(); const [documentsFromKb, documentsFromConnectors] = await Promise.all([ this.recallFromKnowledgeBase({ @@ -303,9 +156,8 @@ export class KnowledgeBaseService { queries, categories, namespace, - modelId, }).catch((error) => { - if (isModelMissingOrUnavailableError(error)) { + if (isInferenceEndpointMissingOrUnavailable(error)) { throwKnowledgeBaseNotReady(error.body); } throw error; @@ -462,7 +314,7 @@ export class KnowledgeBaseService { })), }; } catch (error) { - if (isModelMissingOrUnavailableError(error)) { + if (isInferenceEndpointMissingOrUnavailable(error)) { throwKnowledgeBaseNotReady(error.body); } throw error; @@ -552,14 +404,14 @@ export class KnowledgeBaseService { document: { '@timestamp': new Date().toISOString(), ...doc, + semantic_text: doc.text, user, namespace, }, - pipeline: resourceNames.pipelines.kb, refresh: 'wait_for', }); } catch (error) { - if (error instanceof errors.ResponseError && error.body.error.type === 'status_exception') { + if (isInferenceEndpointMissingOrUnavailable(error)) { throwKnowledgeBaseNotReady(error.body); } throw error; @@ -576,10 +428,66 @@ export class KnowledgeBaseService { return Promise.resolve(); } catch (error) { - if (isModelMissingOrUnavailableError(error)) { + if (isInferenceEndpointMissingOrUnavailable(error)) { throwKnowledgeBaseNotReady(error.body); } throw error; } }; + + getStatus = async () => { + let errorMessage = ''; + const endpoint = await getInferenceEndpoint({ + esClient: this.dependencies.esClient, + logger: this.dependencies.logger, + }).catch((error) => { + if (!isInferenceEndpointMissingOrUnavailable(error)) { + throw error; + } + this.dependencies.logger.error(`Failed to get inference endpoint: ${error.message}`); + errorMessage = error.message; + }); + + const enabled = this.dependencies.enabled; + if (!endpoint) { + return { ready: false, enabled, errorMessage }; + } + + const modelId = endpoint.service_settings?.model_id; + const modelStats = await this.dependencies.esClient.asInternalUser.ml + .getTrainedModelsStats({ model_id: modelId }) + .catch((error) => { + this.dependencies.logger.error(`Failed to get model stats: ${error.message}`); + errorMessage = error.message; + }); + + if (!modelStats) { + return { ready: false, enabled, errorMessage }; + } + + const elserModelStats = modelStats.trained_model_stats.find( + (stats) => stats.deployment_stats?.deployment_id === AI_ASSISTANT_KB_INFERENCE_ID + ); + const deploymentState = elserModelStats?.deployment_stats?.state; + const allocationState = elserModelStats?.deployment_stats?.allocation_status.state; + const allocationCount = + elserModelStats?.deployment_stats?.allocation_status.allocation_count ?? 0; + const ready = + deploymentState === 'started' && allocationState === 'fully_allocated' && allocationCount > 0; + + this.dependencies.logger.debug( + `Model deployment state: ${deploymentState}, allocation state: ${allocationState}, ready: ${ready}` + ); + + return { + endpoint, + ready, + enabled, + model_stats: { + allocation_count: allocationCount, + deployment_state: deploymentState, + allocation_state: allocationState, + }, + }; + }; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/task_manager_definitions/register_migrate_knowledge_base_entries_task.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/task_manager_definitions/register_migrate_knowledge_base_entries_task.ts new file mode 100644 index 0000000000000..3df125ab2ba2d --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/task_manager_definitions/register_migrate_knowledge_base_entries_task.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import pLimit from 'p-limit'; +import { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; +import type { CoreSetup, Logger } from '@kbn/core/server'; +import pRetry from 'p-retry'; +import { KnowledgeBaseEntry } from '../../../common'; +import { resourceNames } from '..'; +import { getInferenceEndpoint } from '../inference_endpoint'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; + +const TASK_ID = 'obs-ai-assistant:knowledge-base-migration-task-id'; +const TASK_TYPE = 'obs-ai-assistant:knowledge-base-migration'; + +// This task will re-index all knowledge base entries without `semantic_text` field +// to ensure the field is populated with the correct embeddings. +// After the migration we will no longer need to use the `ml.tokens` field. +export async function registerMigrateKnowledgeBaseEntriesTask({ + taskManager, + logger, + core, +}: { + taskManager: TaskManagerSetupContract; + logger: Logger; + core: CoreSetup; +}) { + logger.debug(`Register task "${TASK_TYPE}"`); + + const [coreStart, pluginsStart] = await core.getStartServices(); + + taskManager.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Migrate AI Assistant Knowledge Base', + description: `Migrates AI Assistant knowledge base entries`, + timeout: '1h', + maxAttempts: 5, + createTaskRunner() { + return { + async run() { + logger.debug(`Run task: "${TASK_TYPE}"`); + + const esClient = { asInternalUser: coreStart.elasticsearch.client.asInternalUser }; + await runSemanticTextKnowledgeBaseMigration({ esClient, logger }); + }, + }; + }, + }, + }); + + logger.debug(`Scheduled task: "${TASK_TYPE}"`); + await pluginsStart.taskManager.ensureScheduled({ + id: TASK_ID, + taskType: TASK_TYPE, + scope: ['aiAssistant'], + params: {}, + state: {}, + }); +} + +export async function runSemanticTextKnowledgeBaseMigration({ + esClient, + logger, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; +}) { + logger.debug('Knowledge base migration: Running migration'); + + try { + const response = await esClient.asInternalUser.search({ + size: 100, + track_total_hits: true, + index: [resourceNames.aliases.kb], + query: { + bool: { + must_not: { + exists: { + field: 'semantic_text', + }, + }, + }, + }, + _source: { + excludes: ['ml.tokens'], + }, + }); + + if (response.hits.hits.length === 0) { + logger.debug('Knowledge base migration: No remaining entries to migrate'); + return; + } + + logger.debug(`Knowledge base migration: Found ${response.hits.hits.length} entries to migrate`); + + await waitForInferenceEndpoint({ esClient, logger }); + + // Limit the number of concurrent requests to avoid overloading the cluster + const limiter = pLimit(10); + const promises = response.hits.hits.map((hit) => { + return limiter(() => { + if (!hit._source || !hit._id) { + return; + } + + return esClient.asInternalUser.update({ + index: resourceNames.aliases.kb, + id: hit._id, + body: { + doc: { + ...hit._source, + semantic_text: hit._source.text, + }, + }, + }); + }); + }); + + await Promise.all(promises); + logger.debug(`Knowledge base migration: Migrated ${promises.length} entries`); + await runSemanticTextKnowledgeBaseMigration({ esClient, logger }); + } catch (e) { + logger.error('Knowledge base migration: Failed to migrate entries'); + logger.error(e); + } +} + +async function waitForInferenceEndpoint({ + esClient, + logger, +}: { + esClient: { asInternalUser: ElasticsearchClient }; + logger: Logger; +}) { + return pRetry( + async () => { + const endpoint = await getInferenceEndpoint({ esClient, logger }); + if (!endpoint) { + throw new Error('Inference endpoint not yet ready'); + } + }, + { retries: 20, factor: 2 } + ); +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 32e78303e7be3..c9d88a7c0f8ed 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -10091,16 +10091,10 @@ "xpack.aiAssistant.technicalPreviewBadgeDescription": "GTP4 est nécessaire pour bénéficier d'une meilleure expérience avec les appels de fonctions (par exemple lors de la réalisation d'analyse de la cause d'un problème, de la visualisation de données et autres). GPT3.5 peut fonctionner pour certains des workflows les plus simples comme les explications d'erreurs ou pour bénéficier d'une expérience comparable à ChatGPT au sein de Kibana à partir du moment où les appels de fonctions ne sont pas fréquents.", "xpack.aiAssistant.userExecutedFunctionEvent": "a exécuté la fonction {functionName}", "xpack.aiAssistant.userSuggestedFunctionEvent": "a demandé la fonction {functionName}", - "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": "{retryInstallingLink} ou vérifiez {trainedModelsLink} pour vous assurer que {modelName} est déployé et en cours d'exécution.", "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "Configuration de la base de connaissances", "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "Inspecter les problèmes", - "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "Problèmes", "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "La base de connaissances a été installée avec succès", - "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "Le modèle {modelName} n'est pas déployé", - "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "L'état d'allocation de {modelName} est {allocationState}", - "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "L'état de déploiement de {modelName} est {deploymentState}", "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "Installer la base de connaissances", - "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "Modèles entraînés", "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "Nous configurons votre base de connaissances. Cette opération peut prendre quelques minutes. Vous pouvez continuer à utiliser l'Assistant lors de ce processus.", "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "Impossible de charger les connecteurs", "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "Vous n'avez pas les autorisations requises pour charger les connecteurs", @@ -11718,8 +11712,8 @@ "xpack.apm.serviceIcons.service": "Service", "xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "Architecture", "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, =0 {Zone de disponibilité} one {Zone de disponibilité} other {Zones de disponibilité}}", - "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, =0 {Type de déclencheur} one {Type de déclencheur} other {Types de déclencheurs}}", "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, =0 {Nom de fonction} one {Nom de fonction} other {Noms de fonction}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, =0 {Type de déclencheur} one {Type de déclencheur} other {Types de déclencheurs}}", "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, =0{Type de machine} one {Type de machine} other {Types de machines}}", "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "ID de projet", "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "Fournisseur cloud", @@ -28307,8 +28301,8 @@ "xpack.maps.source.esSearch.descendingLabel": "décroissant", "xpack.maps.source.esSearch.extentFilterLabel": "Filtre dynamique pour les données de la zone de carte visible", "xpack.maps.source.esSearch.fieldNotFoundMsg": "Impossible de trouver \"{fieldName}\" dans le modèle d'indexation \"{indexPatternName}\".", - "xpack.maps.source.esSearch.geofieldLabel": "Champ géospatial", "xpack.maps.source.esSearch.geoFieldLabel": "Champ géospatial", + "xpack.maps.source.esSearch.geofieldLabel": "Champ géospatial", "xpack.maps.source.esSearch.geoFieldTypeLabel": "Type de champ géospatial", "xpack.maps.source.esSearch.indexOverOneLengthEditError": "Votre vue de données pointe vers plusieurs index. Un seul index est autorisé par vue de données.", "xpack.maps.source.esSearch.indexZeroLengthEditError": "Votre vue de données ne pointe vers aucun index.", @@ -38084,8 +38078,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibana ne permet qu'un maximum de {maxNumber} {maxNumber, plural, =1 {alerte} other {alertes}} par exécution de règle.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "Nom obligatoire.", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "Ajouter un guide d'investigation sur les règles...", - "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "Ajouter le guide de configuration de règle...", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "Fournissez des instructions sur les conditions préalables à la règle, telles que les intégrations requises, les étapes de configuration et tout ce qui est nécessaire au bon fonctionnement de la règle.", + "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "Ajouter le guide de configuration de règle...", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "Guide de configuration", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "Une balise ne doit pas être vide", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "Le remplacement du préfixe d'indicateur ne peut pas être vide.", @@ -43876,8 +43870,8 @@ "xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "Sélectionner un SLO", "xpack.slo.sloEmbeddable.displayName": "Aperçu du SLO", "xpack.slo.sloEmbeddable.overview.sloNotFoundText": "Le SLO a été supprimé. Vous pouvez supprimer sans risque le widget du tableau de bord.", - "xpack.slo.sloGridItem.targetFlexItemLabel": "Cible {target}", "xpack.slo.sLOGridItem.targetFlexItemLabel": "Cible {target}", + "xpack.slo.sloGridItem.targetFlexItemLabel": "Cible {target}", "xpack.slo.sloGroupConfiguration.customFiltersLabel": "Personnaliser le filtre", "xpack.slo.sloGroupConfiguration.customFiltersOptional": "Facultatif", "xpack.slo.sloGroupConfiguration.customFilterText": "Personnaliser le filtre", @@ -45407,8 +45401,8 @@ "xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - Données de gestion des cas", "xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "Éditeur de code", "xpack.stackConnectors.components.d3security.bodyFieldLabel": "Corps", - "xpack.stackConnectors.components.d3security.connectorTypeTitle": "Données D3", "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security", + "xpack.stackConnectors.components.d3security.connectorTypeTitle": "Données D3", "xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "Type d'événement", "xpack.stackConnectors.components.d3security.invalidActionText": "Nom d'action non valide.", "xpack.stackConnectors.components.d3security.requiredActionText": "L'action est requise.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6607dff1c812a..fffed2d59a462 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10081,16 +10081,10 @@ "xpack.aiAssistant.technicalPreviewBadgeDescription": "関数呼び出し(根本原因分析やデータの視覚化など)を使用する際に、より一貫性のあるエクスペリエンスを実現するために、GPT4が必要です。GPT3.5は、エラーの説明などのシンプルなワークフローの一部や、頻繁な関数呼び出しの使用が必要とされないKibana内のエクスペリエンスなどのChatGPTで機能します。", "xpack.aiAssistant.userExecutedFunctionEvent": "関数{functionName}を実行しました", "xpack.aiAssistant.userSuggestedFunctionEvent": "関数{functionName}を要求しました", - "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": "{retryInstallingLink}か、{trainedModelsLink}を確認して、{modelName}がデプロイされ、実行中であることを確かめてください。", "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "ナレッジベースをセットアップ中", "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "問題を検査", - "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "問題", "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "ナレッジベースは正常にインストールされました", - "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "モデル\"{modelName}\"はデプロイされていません", - "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "\"{modelName}\"の割り当て状態は{allocationState}です", - "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "\"{modelName}\"のデプロイ状態は{deploymentState}です", "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "ナレッジベースをインストール", - "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "学習済みモデル", "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "ナレッジベースをセットアップしています。これには数分かかる場合があります。この処理の実行中には、アシスタントを使用し続けることができます。", "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "コネクターを読み込めませんでした", "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "コネクターを取得するために必要な権限が不足しています", @@ -11701,8 +11695,8 @@ "xpack.apm.serviceIcons.service": "サービス", "xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "アーキテクチャー", "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, other {可用性ゾーン}}", - "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {トリガータイプ}}", "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {関数名}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {トリガータイプ}}", "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, other {コンピュータータイプ} }\n", "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "プロジェクト ID", "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "クラウドプロバイダー", @@ -28279,8 +28273,8 @@ "xpack.maps.source.esSearch.descendingLabel": "降順", "xpack.maps.source.esSearch.extentFilterLabel": "マップの表示範囲でデータを動的にフィルタリング", "xpack.maps.source.esSearch.fieldNotFoundMsg": "インデックスパターン''{indexPatternName}''に''{fieldName}''が見つかりません。", - "xpack.maps.source.esSearch.geofieldLabel": "地理空間フィールド", "xpack.maps.source.esSearch.geoFieldLabel": "地理空間フィールド", + "xpack.maps.source.esSearch.geofieldLabel": "地理空間フィールド", "xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空間フィールドタイプ", "xpack.maps.source.esSearch.indexOverOneLengthEditError": "データビューは複数のインデックスを参照しています。データビューごとに1つのインデックスのみが許可されています。", "xpack.maps.source.esSearch.indexZeroLengthEditError": "データビューはどのインデックスも参照していません。", @@ -38051,8 +38045,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibanaで許可される最大数は、1回の実行につき、{maxNumber} {maxNumber, plural, other {アラート}}です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...", - "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "ルールセットアップガイドを追加...", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "必要な統合、構成ステップ、ルールが正常に動作するために必要な他のすべての項目といった、ルール前提条件に関する指示を入力します。", + "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "ルールセットアップガイドを追加...", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "セットアップガイド", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "タグを空にすることはできません", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "インジケータープレフィックスの無効化を空にすることはできません", @@ -43840,8 +43834,8 @@ "xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "SLOを選択", "xpack.slo.sloEmbeddable.displayName": "SLO概要", "xpack.slo.sloEmbeddable.overview.sloNotFoundText": "SLOが削除されました。ウィジェットをダッシュボードから安全に削除できます。", - "xpack.slo.sloGridItem.targetFlexItemLabel": "目標{target}", "xpack.slo.sLOGridItem.targetFlexItemLabel": "目標{target}", + "xpack.slo.sloGridItem.targetFlexItemLabel": "目標{target}", "xpack.slo.sloGroupConfiguration.customFiltersLabel": "カスタムフィルター", "xpack.slo.sloGroupConfiguration.customFiltersOptional": "オプション", "xpack.slo.sloGroupConfiguration.customFilterText": "カスタムフィルター", @@ -45366,8 +45360,8 @@ "xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webフック - ケース管理データ", "xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "コードエディター", "xpack.stackConnectors.components.d3security.bodyFieldLabel": "本文", - "xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3データ", "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3セキュリティ", + "xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3データ", "xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "イベントタイプ", "xpack.stackConnectors.components.d3security.invalidActionText": "無効なアクション名です。", "xpack.stackConnectors.components.d3security.requiredActionText": "アクションが必要です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 70c3805ff3aaa..4d8de21af735a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9882,16 +9882,10 @@ "xpack.aiAssistant.technicalPreviewBadgeDescription": "需要 GPT4 以在使用函数调用时(例如,执行根本原因分析、数据可视化等时候)获得更加一致的体验。GPT3.5 可作用于某些更简单的工作流(如解释错误),或在 Kibana 中获得不需要频繁使用函数调用的与 ChatGPT 类似的体验。", "xpack.aiAssistant.userExecutedFunctionEvent": "已执行函数 {functionName}", "xpack.aiAssistant.userSuggestedFunctionEvent": "已请求函数 {functionName}", - "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": "{retryInstallingLink} 或检查 {trainedModelsLink},确保 {modelName} 已部署并正在运行。", "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "正在设置知识库", "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "检查问题", - "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "问题", "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "已成功安装知识库", - "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "未部署模型 {modelName}", - "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "{modelName} 的分配状态为 {allocationState}", - "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "{modelName} 的部署状态为 {deploymentState}", "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "安装知识库", - "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "已训练模型", "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "我们正在设置您的知识库。这可能需要若干分钟。此进程处于运行状态时,您可以继续使用该助手。", "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "无法加载连接器", "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "缺少获取连接器所需的权限", @@ -11467,8 +11461,8 @@ "xpack.apm.serviceIcons.service": "服务", "xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "架构", "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, other {可用性区域}}", - "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {触发类型}}", "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {功能名称}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {触发类型}}", "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, other {机器类型}}", "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "项目 ID", "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "云服务提供商", @@ -27784,8 +27778,8 @@ "xpack.maps.source.esSearch.convertToGeoJsonErrorMsg": "无法将搜索响应转换成 geoJson 功能集合,错误:{errorMsg}", "xpack.maps.source.esSearch.descendingLabel": "降序", "xpack.maps.source.esSearch.extentFilterLabel": "在可见地图区域中动态筛留数据", - "xpack.maps.source.esSearch.geofieldLabel": "地理空间字段", "xpack.maps.source.esSearch.geoFieldLabel": "地理空间字段", + "xpack.maps.source.esSearch.geofieldLabel": "地理空间字段", "xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空间字段类型", "xpack.maps.source.esSearch.indexOverOneLengthEditError": "您的数据视图指向多个索引。每个数据视图只允许一个索引。", "xpack.maps.source.esSearch.indexZeroLengthEditError": "您的数据视图未指向任何索引。", @@ -37442,8 +37436,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "每次规则运行时,Kibana 最多只允许 {maxNumber} 个{maxNumber, plural, other {告警}}。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......", - "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "添加规则设置指南......", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "提供有关规则先决条件的说明,如所需集成、配置步骤,以及规则正常运行所需的任何其他内容。", + "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "添加规则设置指南......", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "设置指南", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "标签不得为空", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "指标前缀覆盖不得为空", @@ -43183,8 +43177,8 @@ "xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "选择 SLO", "xpack.slo.sloEmbeddable.displayName": "SLO 概览", "xpack.slo.sloEmbeddable.overview.sloNotFoundText": "SLO 已删除。您可以放心从仪表板中删除小组件。", - "xpack.slo.sloGridItem.targetFlexItemLabel": "目标 {target}", "xpack.slo.sLOGridItem.targetFlexItemLabel": "目标 {target}", + "xpack.slo.sloGridItem.targetFlexItemLabel": "目标 {target}", "xpack.slo.sloGroupConfiguration.customFiltersLabel": "定制筛选", "xpack.slo.sloGroupConfiguration.customFiltersOptional": "可选", "xpack.slo.sloGroupConfiguration.customFilterText": "定制筛选", @@ -44661,8 +44655,8 @@ "xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - 案例管理数据", "xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "代码编辑器", "xpack.stackConnectors.components.d3security.bodyFieldLabel": "正文", - "xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3 数据", "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security", + "xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3 数据", "xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "事件类型", "xpack.stackConnectors.components.d3security.invalidActionText": "操作名称无效。", "xpack.stackConnectors.components.d3security.requiredActionText": "'操作'必填。", diff --git a/x-pack/test/functional/es_archives/observability/ai_assistant/knowledge_base_8_15/data.json.gz b/x-pack/test/functional/es_archives/observability/ai_assistant/knowledge_base_8_15/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ac64be04d3a2356ba2ab90226fba74eb0323acbf GIT binary patch literal 3099 zcmV+$4CM14iwFqm3N2><17u-zVJ>QOZ*Bn1TFZ{)wiVsaSG0TMjcdU7!>de;I6xdE zKs*70Y#J<;x=L=9DC^NZWn=z5hb2UHRl?u z&wYBQU_$LU-!c1<>uvA&AkV{@px>s#(m&GGbVYsQBv4nti{P3&NP z4DDF2);8?xc6_t<=hbv>-~62Zj)%H^SbXyy57ooN@{?FMOTLXybvJIF#IIlD2lk#Z zC8V%Ysc+sI!+t;1&rQe(B}9M89_s;cg$t6eyi-r~0Qwm;!AQ{G8hZ@u(#yQYI* zuM4EW2F{R!^!?Syy|u>Mt!-G|%$fCEJEo`=(andmnoF|pd}wlXur-%XC@;v0AzZE6 z8MsL3t@SxfVOaK;iV3H;_I)@UmzNu1x#i?u`#KDV6y4) za>>Er+&E4=yRY;0wUt(IL(bO>)9DbF7bs>tjzBh83Q*vF+B0dk#<;xOIFGnVa{3{3 z%jJwz+Bvp;whHGuyH*H=Hp&>Rygj+ieZX5Go@8l84|k+PaorO$@$^5 z7V*@Myu?L9f>DRf=EM1WbRtWlCK7(zAGISZVzH%VBXVv}>sSuQl^)I8Q* zzNU7X__2y@OfTo}z4BgZe`_7DX2uP2oZpz|2g*!-*3PLmkdnv)6cBF-^_mxE#+|oQ_#yZn~%CXJO#zyllOW5}t)IC2l*X>QE>7?%SYE~Md-EXc^pq!eCj zsi`^6%k8;x$o`wh7so0K^fNb`cU4Xvjn94t2&O;HBg@ho74^znv@Y1p3Oxewxp@Aed7MsJk_T4~2}U z26(^}X$*-kO$Cw_Ak8Vcb#K$Q^SE*nEd?X~Z_XjNU9_{Cify{FZ-zIlMx`s#Ju)I_ zhQ>ic|5E^H)?+~=<+&%E=%<+Ria8EeK+2{K-El4_l+0OAT#(={d)O$U1?gICJjSJ` z^TfEwv++6Sj!d0~Z>{Ih$IT{uML}VPt)=o#wzmxk51$0vr~1- zIAo;5%eOu~*WiF%E@dZDBiRDMIfAq2n^`7=CrO(q@8VF24xo%B)?5%04wH;@fOE_w z$)dMVggnnV($e!(?BvBO{uwk-x4l$XuvasS&O(hT14FyA4^#@8m!*qJ_vH@kR}*^H zHPzF)iwA==q*%K+t?dH@fg)Ax+NQK7?zpBX7eV4mDWOy~)70J^b4Dp-8~W2WDt$?F zI7dol2z~95j#(H01eqf-?~#vFVSobuH-!E4NxkJV2KwWQ+w1l9FcyYrH0V9u;A+os zgzjm$BY9g^lqgvRk^>&NU37|5%nSmF?TJe0o9Z=+QgcR%vR?^?N5Lx6f%`U0XjSLT zZ+HT1LH3XR+-4o3ypzt6M9d3TRP_R4iS)`o4$}~t91ge}!6e=u>c>Llv;d#9@(cD= zPdTYIBl)^48LsHY(S0;1EeYAsAjeA?%@rXmLq=|qj<2mI#^mMP88lj}-u8`QK9qwo zv}I17o{{ZgCAx{vZGE%!EU;8C`EBUN>QomV$XJ3otK9b5BeYQU+7z`;#Da9L@w9SD zZ*F!!#Xctw2$d2fkCO~|MKe%sMv2AP6s$sxfmx8QI6k5Am!1zw#`QKVW9qB9|8TGZ zvVC7y4eB@}JwqLWk6K3-HAAidq?Lo_+@|k1U2^6qf#4G!KVHuJn6U&jQZ7@RVhob(RsFn_il!h*+?2 zM3=l`8OV@4KO7F$ip_AU zNiKEUW^_TE6<~B5iA$WPd7tkjktD*c=Fj=RUWb9H`H%gg5mzm~TKjq5Hq|{HE)MaU z#rNy%^I@FN$dBfig%z(0r}|S$*!z#C`Ywg>>+x=tYaGT|M0WkpTGa^!^iz!|Mj8z?&XKT zp4?xa|NgfRFFy`*-Cln9p#Sv1-cCR>Pr9$$Fi!AF@#VYn@6Yc(zk8Q<{4WR5!5)A# zF6%d_QBotpX?xRgOpOC~rc zy|xZn7I?Hf?;RNeMx3||c&+v*QbOQ{_!ADEy8`?U!AH8nG}MqDtF{o>W!rT3^(9`l zv_MzY;2T@a|9LJWRB$?E;|&=^+aDE#U>LN_BQS z!YtZaLBwrq@VQx!C=m!x#Rt->eCE(1Nh~@+UdOW>5|3$8VTrg+Wm3m*0H{1tBXvC2 z(~#YrKBYi|v77nBsiaDp(UJ-{)#W<@@{nm|$qX0}(BwHe7oKrV*jWR3?aHJRWo!gt zNX@yA0Qh~OkIVrIw*^smhqg?jolbj@lp%AgG)`4iU~Oh&!deS1P3AOlMdT#PnemcL zTA-fBA{96!sh4zXuZS2qe&n$d>lC-_M5G!huD_);wLDW%|yfX*p`3>PV!CLNZD zO|a$X{8><^AvI}kVL!%pejQx_xrwgno30C0TLdq2F3ku@JU|VGjP4GKky}nrfex0_ z1sy8&8S05EE?utd2H_NRhpEYzU{G#kXmBlZ{$6X3WG9#(GADvTb4li5kMjUT9g3)v zv=9y;uh+gd78tPHqs6}JoHw-nxBvHshZe>1Y4^#Q2cuRx|NBUzq(2^jljsV!YAD_n pzuqBP?-m02)fW`d$9TE>aYAdk`*}`-*#B$k^B+ { await createKnowledgeBaseModel(ml); await observabilityAIAssistantAPIClient - .editor({ + .admin({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, }) .expect(200); @@ -72,6 +79,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { await deleteActionConnector({ supertest, connectorId, log }); await deleteKnowledgeBaseModel(ml); await clearKnowledgeBase(es); + await deleteInferenceEndpoint({ es }); }); it('persists entry in knowledge base', async () => { diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts index 91286dab811fc..fa1f15ddca4cd 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts @@ -6,6 +6,7 @@ */ import { Client } from '@elastic/elasticsearch'; +import { AI_ASSISTANT_KB_INFERENCE_ID } from '@kbn/observability-ai-assistant-plugin/server/service/inference_endpoint'; import { MachineLearningProvider } from '../../../api_integration/services/ml'; import { SUPPORTED_TRAINED_MODELS } from '../../../functional/services/ml/api'; @@ -54,3 +55,19 @@ export async function clearConversations(es: Client) { refresh: true, }); } + +export async function deleteInferenceEndpoint({ + es, + name = AI_ASSISTANT_KB_INFERENCE_ID, +}: { + es: Client; + name?: string; +}) { + return es.transport.request({ + method: 'DELETE', + path: `_inference/sparse_embedding/${name}`, + querystring: { + force: true, + }, + }); +} diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts index 27659f62ad579..8d8c2e2417686 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts @@ -8,7 +8,13 @@ import expect from '@kbn/expect'; import { type KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { clearKnowledgeBase, createKnowledgeBaseModel, deleteKnowledgeBaseModel } from './helpers'; +import { + TINY_ELSER, + clearKnowledgeBase, + createKnowledgeBaseModel, + deleteInferenceEndpoint, + deleteKnowledgeBaseModel, +} from './helpers'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); @@ -20,12 +26,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { await createKnowledgeBaseModel(ml); await observabilityAIAssistantAPIClient - .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' }) + .admin({ + endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, + }) .expect(200); }); after(async () => { await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es }); await clearKnowledgeBase(es); }); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_migration.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_migration.spec.ts new file mode 100644 index 0000000000000..46638d8eebe19 --- /dev/null +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_migration.spec.ts @@ -0,0 +1,160 @@ +/* + * 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 { orderBy } from 'lodash'; +import expect from '@kbn/expect'; +import { AI_ASSISTANT_KB_INFERENCE_ID } from '@kbn/observability-ai-assistant-plugin/server/service/inference_endpoint'; +import { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + deleteKnowledgeBaseModel, + createKnowledgeBaseModel, + clearKnowledgeBase, + deleteInferenceEndpoint, + TINY_ELSER, +} from './helpers'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const ml = getService('ml'); + + const archive = + 'x-pack/test/functional/es_archives/observability/ai_assistant/knowledge_base_8_15'; + + describe('When there are knowledge base entries (from 8.15 or earlier) that does not contain semantic_text embeddings', () => { + before(async () => { + await clearKnowledgeBase(es); + await esArchiver.load(archive); + await createKnowledgeBaseModel(ml); + await observabilityAIAssistantAPIClient + .admin({ + endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, + }) + .expect(200); + }); + + after(async () => { + await clearKnowledgeBase(es); + await esArchiver.unload(archive); + await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es }); + }); + + async function getKnowledgeBaseEntries() { + const res = (await es.search({ + index: '.kibana-observability-ai-assistant-kb*', + body: { + query: { + match_all: {}, + }, + }, + })) as SearchResponse< + KnowledgeBaseEntry & { + semantic_text: { + text: string; + inference: { inference_id: string; chunks: Array<{ text: string; embeddings: any }> }; + }; + } + >; + + return res.hits.hits; + } + + describe('before migrating', () => { + it('the docs do not have semantic_text embeddings', async () => { + const hits = await getKnowledgeBaseEntries(); + const hasSemanticTextEmbeddings = hits.some((hit) => hit._source?.semantic_text); + expect(hasSemanticTextEmbeddings).to.be(false); + }); + }); + + describe('after migrating', () => { + before(async () => { + await observabilityAIAssistantAPIClient + .editor({ + endpoint: 'POST /internal/observability_ai_assistant/kb/semantic_text_migration', + }) + .expect(200); + }); + + it('the docs have semantic_text embeddings', async () => { + const hits = await getKnowledgeBaseEntries(); + const hasSemanticTextEmbeddings = hits.every((hit) => hit._source?.semantic_text); + expect(hasSemanticTextEmbeddings).to.be(true); + + expect( + orderBy(hits, '_source.title').map(({ _source }) => { + const { text, inference } = _source?.semantic_text!; + + return { + text, + inferenceId: inference.inference_id, + chunkCount: inference.chunks.length, + }; + }) + ).to.eql([ + { + text: 'To infinity and beyond!', + inferenceId: AI_ASSISTANT_KB_INFERENCE_ID, + chunkCount: 1, + }, + { + text: "The user's favourite color is blue.", + inferenceId: AI_ASSISTANT_KB_INFERENCE_ID, + chunkCount: 1, + }, + ]); + }); + + it('returns entries correctly via API', async () => { + await observabilityAIAssistantAPIClient + .editor({ + endpoint: 'POST /internal/observability_ai_assistant/kb/semantic_text_migration', + }) + .expect(200); + + const res = await observabilityAIAssistantAPIClient + .editor({ + endpoint: 'GET /internal/observability_ai_assistant/kb/entries', + params: { + query: { + query: '', + sortBy: 'title', + sortDirection: 'asc', + }, + }, + }) + .expect(200); + + expect( + res.body.entries.map(({ title, text, role, type }) => ({ title, text, role, type })) + ).to.eql([ + { + role: 'user_entry', + title: 'Toy Story quote', + type: 'contextual', + text: 'To infinity and beyond!', + }, + { + role: 'assistant_summarization', + title: "User's favourite color", + type: 'contextual', + text: "The user's favourite color is blue.", + }, + ]); + }); + }); + }); +} diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts index 77f010d851f3c..b8cacaaa58351 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts @@ -7,30 +7,58 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteKnowledgeBaseModel, createKnowledgeBaseModel } from './helpers'; +import { + deleteKnowledgeBaseModel, + createKnowledgeBaseModel, + TINY_ELSER, + deleteInferenceEndpoint, +} from './helpers'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); + const es = getService('es'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); describe('/internal/observability_ai_assistant/kb/setup', () => { - it('returns empty object when successful', async () => { + it('returns model info when successful', async () => { await createKnowledgeBaseModel(ml); const res = await observabilityAIAssistantAPIClient - .editor({ + .admin({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, }) .expect(200); - expect(res.body).to.eql({}); + + expect(res.body.service_settings.model_id).to.be('pt_tiny_elser'); + expect(res.body.inference_id).to.be('ai_assistant_kb_inference'); + await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es }); }); - it('returns bad request if model cannot be installed', async () => { - await observabilityAIAssistantAPIClient - .editor({ + it('returns error message if model is not deployed', async () => { + const res = await observabilityAIAssistantAPIClient + .admin({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, }) - .expect(400); + .expect(500); + + // @ts-expect-error + expect(res.body.message).to.include.string( + 'No known trained model with model_id [pt_tiny_elser]' + ); + + // @ts-expect-error + expect(res.body.statusCode).to.be(500); }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts index 6561c416f02cf..76ad2d06e344a 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts @@ -7,38 +7,66 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteKnowledgeBaseModel, createKnowledgeBaseModel, TINY_ELSER } from './helpers'; +import { + deleteKnowledgeBaseModel, + createKnowledgeBaseModel, + TINY_ELSER, + deleteInferenceEndpoint, +} from './helpers'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); + const es = getService('es'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); describe('/internal/observability_ai_assistant/kb/status', () => { - before(async () => { + beforeEach(async () => { await createKnowledgeBaseModel(ml); await observabilityAIAssistantAPIClient - .editor({ + .admin({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, }) .expect(200); }); - after(async () => { - await deleteKnowledgeBaseModel(ml); + afterEach(async () => { + await deleteKnowledgeBaseModel(ml).catch((e) => {}); + await deleteInferenceEndpoint({ es }).catch((e) => {}); }); it('returns correct status after knowledge base is setup', async () => { + const res = await observabilityAIAssistantAPIClient + .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/status' }) + .expect(200); + + expect(res.body.ready).to.be(true); + expect(res.body.enabled).to.be(true); + expect(res.body.endpoint?.service_settings?.model_id).to.eql(TINY_ELSER.id); + }); + + it('returns correct status after model is deleted', async () => { + await deleteKnowledgeBaseModel(ml); + const res = await observabilityAIAssistantAPIClient .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/status', }) .expect(200); - expect(res.body.deployment_state).to.eql('started'); - expect(res.body.model_name).to.eql(TINY_ELSER.id); + + expect(res.body.ready).to.be(false); + expect(res.body.enabled).to.be(true); + expect(res.body.errorMessage).to.include.string( + 'No known trained model with model_id [pt_tiny_elser]' + ); }); - it('returns correct status after elser is stopped', async () => { - await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true); + it('returns correct status after inference endpoint is deleted', async () => { + await deleteInferenceEndpoint({ es }); const res = await observabilityAIAssistantAPIClient .editor({ @@ -46,11 +74,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { }) .expect(200); - expect(res.body).to.eql({ - ready: false, - model_name: TINY_ELSER.id, - enabled: true, - }); + expect(res.body.ready).to.be(false); + expect(res.body.enabled).to.be(true); + expect(res.body.errorMessage).to.include.string( + 'Inference endpoint not found [ai_assistant_kb_inference]' + ); }); }); } 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 a9c1f245a1ac3..04791909340ef 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 @@ -12,9 +12,11 @@ import { CONTEXT_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/se import { Instruction } from '@kbn/observability-ai-assistant-plugin/common/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { + TINY_ELSER, clearConversations, clearKnowledgeBase, createKnowledgeBaseModel, + deleteInferenceEndpoint, deleteKnowledgeBaseModel, } from './helpers'; import { getConversationCreatedEvent } from '../conversations/helpers'; @@ -33,14 +35,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('Knowledge base user instructions', () => { before(async () => { await createKnowledgeBaseModel(ml); - await observabilityAIAssistantAPIClient - .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' }) + .admin({ + endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, + }) .expect(200); }); after(async () => { await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es }); await clearKnowledgeBase(es); await clearConversations(es); }); diff --git a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts index 7a5a51ae58b6a..300fec6aa45aa 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/knowledge_base_management/index.spec.ts @@ -8,8 +8,10 @@ import expect from '@kbn/expect'; import { subj as testSubjSelector } from '@kbn/test-subj-selector'; import { + TINY_ELSER, clearKnowledgeBase, createKnowledgeBaseModel, + deleteInferenceEndpoint, deleteKnowledgeBaseModel, } from '../../../observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; import { ObservabilityAIAssistantApiClient } from '../../../observability_ai_assistant_api_integration/common/observability_ai_assistant_api_client'; @@ -56,7 +58,14 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte await Promise.all([ // setup the knowledge base observabilityAIAssistantAPIClient - .editor({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' }) + .admin({ + endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, + }) .expect(200), // login as editor @@ -65,7 +74,12 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte }); after(async () => { - await Promise.all([deleteKnowledgeBaseModel(ml), clearKnowledgeBase(es), ui.auth.logout()]); + await Promise.all([ + deleteKnowledgeBaseModel(ml), + deleteInferenceEndpoint({ es }), + clearKnowledgeBase(es), + ui.auth.logout(), + ]); }); describe('when the LLM calls the "summarize" function for two different users', () => { diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index c8056c2ee205e..88ef256b353e6 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -151,6 +151,7 @@ export default function ({ getService }: FtrProviderContext) { 'fleet:update_agent_tags:retry', 'fleet:upgrade_action:retry', 'logs-data-telemetry', + 'obs-ai-assistant:knowledge-base-migration', 'osquery:telemetry-configs', 'osquery:telemetry-packs', 'osquery:telemetry-saved-queries', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts index 36a60f29cb6ce..14078f228c7c8 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/config.ts @@ -29,6 +29,6 @@ export default createTestConfig({ // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml esServerArgs: ['xpack.ml.dfa.enabled=false'], kbnServerArgs: [ - `--xpack.observabilityAIAssistant.modelId=${SUPPORTED_TRAINED_MODELS.TINY_ELSER.name}`, + `--xpack.observabilityAIAssistant.modelId=${SUPPORTED_TRAINED_MODELS.TINY_ELSER.name}`, // TODO: Remove ], }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/helpers.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/helpers.ts deleted file mode 100644 index 6affeeb861295..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MachineLearningProvider } from '@kbn/test-suites-xpack/api_integration/services/ml'; -import { SUPPORTED_TRAINED_MODELS } from '@kbn/test-suites-xpack/functional/services/ml/api'; - -export const TINY_ELSER = { - ...SUPPORTED_TRAINED_MODELS.TINY_ELSER, - id: SUPPORTED_TRAINED_MODELS.TINY_ELSER.name, -}; - -export async function deleteKnowledgeBaseModel(ml: ReturnType) { - await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true); - await ml.api.deleteTrainedModelES(TINY_ELSER.id); - await ml.testResources.cleanMLSavedObjects(); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts index ce46939c365be..8f3569c9fc959 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base.spec.ts @@ -9,8 +9,9 @@ import expect from '@kbn/expect'; import { clearKnowledgeBase, createKnowledgeBaseModel, + deleteInferenceEndpoint, + deleteKnowledgeBaseModel, } from '@kbn/test-suites-xpack/observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; -import { deleteKnowledgeBaseModel } from './helpers'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import type { InternalRequestHeader, RoleCredentials } from '../../../../../../shared/services'; @@ -24,7 +25,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { // TODO: https://github.com/elastic/kibana/issues/192886 describe.skip('Knowledge base', function () { - // TODO: https://github.com/elastic/kibana/issues/192757 this.tags(['skipMKI']); let roleAuthc: RoleCredentials; let internalReqHeader: InternalRequestHeader; @@ -36,6 +36,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es }); await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts index ad5dd32f5c0b4..a792b01b0e2cb 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_setup.spec.ts @@ -6,49 +6,80 @@ */ import expect from '@kbn/expect'; -import { createKnowledgeBaseModel } from '@kbn/test-suites-xpack/observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; -import { deleteKnowledgeBaseModel } from './helpers'; +import { + createKnowledgeBaseModel, + deleteInferenceEndpoint, + TINY_ELSER, + deleteKnowledgeBaseModel, +} from '@kbn/test-suites-xpack/observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import type { InternalRequestHeader, RoleCredentials } from '../../../../../../shared/services'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); + const es = getService('es'); const svlUserManager = getService('svlUserManager'); const svlCommonApi = getService('svlCommonApi'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); describe('/internal/observability_ai_assistant/kb/setup', function () { - // TODO: https://github.com/elastic/kibana/issues/192757 this.tags(['skipMKI']); let roleAuthc: RoleCredentials; let internalReqHeader: InternalRequestHeader; + before(async () => { - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('editor'); + await deleteKnowledgeBaseModel(ml).catch(() => {}); + await deleteInferenceEndpoint({ es }).catch(() => {}); + + roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); internalReqHeader = svlCommonApi.getInternalRequestHeader(); }); + after(async () => { await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); + it('returns empty object when successful', async () => { await createKnowledgeBaseModel(ml); const res = await observabilityAIAssistantAPIClient .slsUser({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, roleAuthc, internalReqHeader, }) .expect(200); - expect(res.body).to.eql({}); + + expect(res.body.service_settings.model_id).to.be('pt_tiny_elser'); + expect(res.body.inference_id).to.be('ai_assistant_kb_inference'); + await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es }); }); + it('returns bad request if model cannot be installed', async () => { - await observabilityAIAssistantAPIClient + const res = await observabilityAIAssistantAPIClient .slsUser({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, roleAuthc, internalReqHeader, }) - .expect(400); + .expect(500); + + // @ts-expect-error + expect(res.body.message).to.include.string( + 'No known trained model with model_id [pt_tiny_elser]' + ); }); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts index 60e7c743bbbbb..a7a7b55cf4e02 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_status.spec.ts @@ -7,31 +7,39 @@ import expect from '@kbn/expect'; import { + deleteInferenceEndpoint, createKnowledgeBaseModel, TINY_ELSER, + deleteKnowledgeBaseModel, } from '@kbn/test-suites-xpack/observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; -import { deleteKnowledgeBaseModel } from './helpers'; +import { AI_ASSISTANT_KB_INFERENCE_ID } from '@kbn/observability-ai-assistant-plugin/server/service/inference_endpoint'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import type { InternalRequestHeader, RoleCredentials } from '../../../../../../shared/services'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); + const es = getService('es'); const svlUserManager = getService('svlUserManager'); const svlCommonApi = getService('svlCommonApi'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); describe('/internal/observability_ai_assistant/kb/status', function () { - // TODO: https://github.com/elastic/kibana/issues/192757 this.tags(['skipMKI']); let roleAuthc: RoleCredentials; let internalReqHeader: InternalRequestHeader; + before(async () => { - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('editor'); + roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); internalReqHeader = svlCommonApi.getInternalRequestHeader(); await createKnowledgeBaseModel(ml); await observabilityAIAssistantAPIClient .slsUser({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, roleAuthc, internalReqHeader, }) @@ -40,6 +48,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es, name: AI_ASSISTANT_KB_INFERENCE_ID }).catch((err) => {}); await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); @@ -51,12 +60,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { internalReqHeader, }) .expect(200); - expect(res.body.deployment_state).to.eql('started'); - expect(res.body.model_name).to.eql(TINY_ELSER.id); + + expect(res.body.enabled).to.be(true); + expect(res.body.ready).to.be(true); + expect(res.body.endpoint?.service_settings?.model_id).to.eql(TINY_ELSER.id); }); it('returns correct status after elser is stopped', async () => { - await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true); + await deleteInferenceEndpoint({ es, name: AI_ASSISTANT_KB_INFERENCE_ID }); const res = await observabilityAIAssistantAPIClient .slsUser({ @@ -66,11 +77,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }) .expect(200); - expect(res.body).to.eql({ - ready: false, - model_name: TINY_ELSER.id, - enabled: true, - }); + expect(res.body.enabled).to.be(true); + expect(res.body.ready).to.be(false); }); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_user_instructions.spec.ts index 4181b6a14ffde..fc9864ac8768c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -13,13 +13,14 @@ import { clearConversations, clearKnowledgeBase, createKnowledgeBaseModel, + deleteInferenceEndpoint, + deleteKnowledgeBaseModel, } from '@kbn/test-suites-xpack/observability_ai_assistant_api_integration/tests/knowledge_base/helpers'; import { getConversationCreatedEvent } from '@kbn/test-suites-xpack/observability_ai_assistant_api_integration/tests/conversations/helpers'; import { LlmProxy, createLlmProxy, } from '@kbn/test-suites-xpack/observability_ai_assistant_api_integration/common/create_llm_proxy'; -import { deleteKnowledgeBaseModel } from './helpers'; import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import type { InternalRequestHeader, RoleCredentials } from '../../../../../../shared/services'; @@ -59,6 +60,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await deleteKnowledgeBaseModel(ml); + await deleteInferenceEndpoint({ es }); await clearKnowledgeBase(es); await clearConversations(es); await svlUserManager.invalidateM2mApiKeyWithRoleScope(johnRoleAuthc); From 2e9926de3047ccd47f686406e4f1308d1c38df73 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Thu, 14 Nov 2024 13:31:17 +0100 Subject: [PATCH 07/23] [APM] Migrate service tests to deployment agnostic (#199812) ## Summary Closes https://github.com/elastic/kibana/issues/198988 Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate `service` test folder to Deployment-agnostic testing strategy. >[!NOTE] > `top_services.spec.ts` and `throughput.spec.ts` were partially migrated and `annotations.spec.ts` was not migrated ### 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" ``` - [ ] ~(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 --------- Co-authored-by: Sergi Romeu --- .../apm/alerts/error_count_threshold.spec.ts | 13 +- .../apm/alerts/transaction_duration.spec.ts | 13 +- .../apm/alerts/transaction_error_rate.spec.ts | 13 +- .../observability/apm/constants/archiver.ts | 4 - .../apis/observability/apm/index.ts | 1 + ...error_groups_detailed_statistics.spec.snap | 0 ...chive_services_detailed_statistics.spec.ts | 49 ++ .../apm/services/derived_annotations.spec.ts | 172 ++++++ .../error_groups_detailed_statistics.spec.ts | 23 +- .../error_groups_main_statistics.spec.ts | 23 +- .../services/error_groups/generate_data.ts | 0 .../error_groups/get_error_group_ids.ts | 4 +- .../get_service_node_metadata.spec.ts | 32 +- .../apis/observability/apm/services/index.ts | 24 + .../apm}/services/service_alerts.spec.ts | 96 ++-- .../services/service_details/generate_data.ts | 0 .../service_details/service_details.spec.ts | 120 ++++ .../service_infra_metrics.spec.ts | 28 +- .../services/service_icons/generate_data.ts | 0 .../service_icons/service_icons.spec.ts | 80 +++ .../services_detailed_statistics.spec.ts | 119 ++-- .../apm/services/throughput.spec.ts | 541 ++++++++++++++++++ .../apm/services/top_services.spec.ts | 364 ++++++++++++ .../apm/services/transaction_types.spec.ts | 92 +++ .../services/alerting_api.ts | 19 + .../tests/services/agent.spec.ts | 59 -- ...chive_services_detailed_statistics.spec.ts | 32 -- .../services/derived_annotations.spec.ts | 171 ------ .../service_details/service_details.spec.ts | 121 ---- .../service_icons/service_icons.spec.ts | 77 --- .../tests/services/throughput.spec.ts | 535 ----------------- .../tests/services/top_services.spec.ts | 351 ------------ .../tests/services/transaction_types.spec.ts | 92 --- 33 files changed, 1634 insertions(+), 1634 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/__snapshots__/error_groups_detailed_statistics.spec.snap (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/error_groups_detailed_statistics.spec.ts (91%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/error_groups_main_statistics.spec.ts (83%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/generate_data.ts (100%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/error_groups/get_error_group_ids.ts (88%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/get_service_node_metadata.spec.ts (79%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_alerts.spec.ts (63%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_details/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_details/service_infra_metrics.spec.ts (89%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/service_icons/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/services/services_detailed_statistics.spec.ts (87%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/agent.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/throughput.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts 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 16493e8220f68..e0b9e1b022b4f 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 @@ -11,7 +11,7 @@ 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 { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { fetchServiceInventoryAlertCounts, @@ -23,7 +23,6 @@ import { } from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { - const roleScopedSupertest = getService('roleScopedSupertest'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); const alertingApi = getService('alertingApi'); @@ -31,7 +30,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('error count threshold alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; const javaErrorMessage = 'a java error'; @@ -52,14 +50,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon }; before(async () => { - supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( - 'viewer', - { - withInternalHeaders: true, - useCookieHeader: true, - } - ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); const opbeansJava = apm @@ -116,7 +106,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts index 0cd3446359557..0fab5d6e35853 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_duration.spec.ts @@ -12,7 +12,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { fetchServiceInventoryAlertCounts, @@ -24,7 +24,6 @@ import { } from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { - const roleScopedSupertest = getService('roleScopedSupertest'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); const alertingApi = getService('alertingApi'); @@ -43,18 +42,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('transaction duration alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; before(async () => { - supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( - 'viewer', - { - withInternalHeaders: true, - useCookieHeader: true, - } - ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); const opbeansJava = apm @@ -86,7 +76,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts index e538ff0e6a3ba..637786195cd21 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/alerts/transaction_error_rate.spec.ts @@ -11,7 +11,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import type { RoleCredentials, SupertestWithRoleScopeType } from '../../../../services'; +import type { RoleCredentials } from '../../../../services'; import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { fetchServiceInventoryAlertCounts, @@ -23,7 +23,6 @@ import { } from './helpers/alerting_helper'; export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { - const roleScopedSupertest = getService('roleScopedSupertest'); const apmApiClient = getService('apmApi'); const synthtrace = getService('synthtrace'); const alertingApi = getService('alertingApi'); @@ -31,18 +30,9 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon describe('transaction error rate alert', () => { let apmSynthtraceEsClient: ApmSynthtraceEsClient; - let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; let roleAuthc: RoleCredentials; before(async () => { - supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( - 'viewer', - { - withInternalHeaders: true, - useCookieHeader: true, - } - ); - roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); const opbeansJava = apm @@ -84,7 +74,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon after(async () => { await apmSynthtraceEsClient.clean(); - await supertestViewerWithCookieCredentials.destroy(); await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts index 6afc2e9eca63b..1c81efc0966d0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts @@ -7,24 +7,20 @@ type ArchiveName = | '8.0.0' - | 'apm_8.0.0' | 'apm_mappings_only_8.0.0' | 'infra_metrics_and_apm' | 'metrics_8.0.0' - | 'ml_8.0.0' | 'observability_overview' | 'rum_8.0.0' | 'rum_test_data'; export const ARCHIVER_ROUTES: { [key in ArchiveName]: string } = { '8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0', - 'apm_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0', 'apm_mappings_only_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0', infra_metrics_and_apm: 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/infra_metrics_and_apm', 'metrics_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0', - 'ml_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/ml_8.0.0', observability_overview: 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview', 'rum_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0', 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 0cf1116f4140a..c09e1dd4b99b4 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('./services')); loadTestFile(require.resolve('./historical_data')); loadTestFile(require.resolve('./observability_overview')); loadTestFile(require.resolve('./latency')); diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_detailed_statistics.spec.snap b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/__snapshots__/error_groups_detailed_statistics.spec.snap similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_detailed_statistics.spec.snap rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/__snapshots__/error_groups_detailed_statistics.spec.snap diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts new file mode 100644 index 0000000000000..9c00ed2a471bf --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/archive_services_detailed_statistics.spec.ts @@ -0,0 +1,49 @@ +/* + * 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 { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + + const start = '2021-10-01T00:00:00.000Z'; + const end = '2021-10-01T01:00:00.000Z'; + + const serviceNames = ['opbeans-java', 'opbeans-go']; + + describe('Services detailed statistics', () => { + describe('Services detailed statistics when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `POST /internal/apm/services/detailed_statistics`, + params: { + query: { + start, + end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + offset: '1d', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + }, + body: { + serviceNames: JSON.stringify(serviceNames), + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.be.empty(); + expect(response.body.previousPeriod).to.be.empty(); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts new file mode 100644 index 0000000000000..3af97dea84c72 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/derived_annotations.spec.ts @@ -0,0 +1,172 @@ +/* + * 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; + +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function annotationApiTests({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const es = getService('es'); + const samlAuth = getService('samlAuth'); + + const dates = [ + new Date('2021-02-01T00:00:00.000Z'), + new Date('2021-02-01T01:00:00.000Z'), + new Date('2021-02-01T02:00:00.000Z'), + new Date('2021-02-01T03:00:00.000Z'), + ]; + + const indexName = 'apm-8.0.0-transaction'; + + describe('Derived deployment annotations', () => { + describe('when there are multiple service versions', () => { + let roleAuthc: RoleCredentials; + let response: APIReturnType<'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31'>; + + before(async () => { + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('viewer'); + const indexExists = await es.indices.exists({ index: indexName }); + if (indexExists) { + await es.indices.delete({ + index: indexName, + }); + } + + await es.indices.create({ + index: indexName, + body: { + mappings: { + properties: { + service: { + properties: { + name: { + type: 'keyword', + }, + version: { + type: 'keyword', + }, + environment: { + type: 'keyword', + }, + }, + }, + transaction: { + properties: { + type: { + type: 'keyword', + }, + duration: { + type: 'long', + }, + }, + }, + processor: { + properties: { + event: { + type: 'keyword', + }, + }, + }, + }, + }, + }, + }); + + const docs = dates.flatMap((date, index) => { + const baseAnnotation = { + transaction: { + type: 'request', + duration: 1000000, + }, + + service: { + name: 'opbeans-java', + environment: 'production', + version: index + 1, + }, + processor: { + event: 'transaction', + }, + }; + return [ + { + ...baseAnnotation, + '@timestamp': date.toISOString(), + }, + { + ...baseAnnotation, + '@timestamp': new Date(date.getTime() + 30000), + }, + { + ...baseAnnotation, + '@timestamp': new Date(date.getTime() + 60000), + }, + ]; + }); + + await es.bulk({ + index: indexName, + body: docs.flatMap((doc) => [{ index: {} }, doc]), + refresh: true, + }); + + response = ( + await apmApiClient.readUser({ + endpoint: 'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + start: dates[1].toISOString(), + end: dates[2].toISOString(), + environment: 'production', + }, + }, + roleAuthc, + }) + ).body; + }); + + after(async () => { + await es.indices.delete({ + index: indexName, + }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + it('annotations are displayed for the service versions in the given time range', async () => { + expect(response.annotations.length).to.be(2); + expect(response.annotations[0]['@timestamp']).to.be(dates[1].getTime()); + expect(response.annotations[1]['@timestamp']).to.be(dates[2].getTime()); + + expectSnapshot(response.annotations[0]).toMatchInline(` + Object { + "@timestamp": 1612141200000, + "id": "2", + "text": "2", + "type": "version", + } + `); + }); + + it('annotations are not displayed for the service versions outside of the given time range', () => { + expect( + response.annotations.some((annotation) => { + return ( + annotation['@timestamp'] !== dates[0].getTime() && + annotation['@timestamp'] !== dates[2].getTime() + ); + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_detailed_statistics.spec.ts similarity index 91% rename from x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_detailed_statistics.spec.ts index 620d705f4463f..e01ea0b618deb 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_detailed_statistics.spec.ts @@ -14,17 +14,17 @@ import { APIReturnType, } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { config, generateData } from './generate_data'; import { getErrorGroupIds } from './get_error_group_ids'; type ErrorGroupsDetailedStatistics = APIReturnType<'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics'>; -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 serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -52,23 +52,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Error groups detailed statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Error groups detailed statistics', () => { + describe('when data is not loaded', () => { it('handles empty state', async () => { const response = await callApi(); expect(response.status).to.be(200); expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); }); - } - ); + }); - // FLAKY: https://github.com/elastic/kibana/issues/177656 - registry.when('Error groups detailed statistics', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE } = config; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_main_statistics.spec.ts similarity index 83% rename from x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_main_statistics.spec.ts index 3377cdabb3847..f6f8f99140c65 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/error_groups_main_statistics.spec.ts @@ -11,16 +11,16 @@ import { APIReturnType, } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; import { generateData, config } from './generate_data'; type ErrorGroupsMainStatistics = APIReturnType<'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics'>; -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 serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -46,24 +46,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Error groups main statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Error groups main statistics', () => { + describe(' when data is not loaded', () => { it('handles empty state', async () => { const response = await callApi(); expect(response.status).to.be(200); expect(response.body.errorGroups).to.empty(); }); - } - ); + }); - // FLAKY: https://github.com/elastic/kibana/issues/177664 - registry.when('Error groups main statistics', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE, ERROR_NAME_1, ERROR_NAME_2 } = config; before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/generate_data.ts diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/get_error_group_ids.ts similarity index 88% rename from x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/get_error_group_ids.ts index 914f6962d9bce..e9487e5e099c9 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/error_groups/get_error_group_ids.ts @@ -5,7 +5,7 @@ * 2.0. */ import { take } from 'lodash'; -import { ApmServices } from '../../../common/config'; +import type { ApmApiClient } from '../../../../../services/apm_api'; export async function getErrorGroupIds({ apmApiClient, @@ -14,7 +14,7 @@ export async function getErrorGroupIds({ serviceName = 'opbeans-java', count = 5, }: { - apmApiClient: Awaited>; + apmApiClient: ApmApiClient; start: number; end: number; serviceName?: string; diff --git a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/get_service_node_metadata.spec.ts similarity index 79% rename from x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/get_service_node_metadata.spec.ts index 6644ed8bc7d1c..e1ac83609b86f 100644 --- a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/get_service_node_metadata.spec.ts @@ -9,12 +9,12 @@ import expect from '@kbn/expect'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - 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; @@ -38,10 +38,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - registry.when( - 'Service node metadata when data is not loaded', - { config: 'basic', archives: [] }, - () => { + describe('Service node metadata', () => { + describe('when data is not loaded', () => { it('handles the empty state', async () => { const response = await callApi(); @@ -54,15 +52,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); }); - } - ); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; - // FLAKY: https://github.com/elastic/kibana/issues/177513 - registry.when( - 'Service node metadata when data is loaded', - { config: 'basic', archives: [] }, - () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const instance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance(instanceName); @@ -94,6 +90,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { } `); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts new file mode 100644 index 0000000000000..2beba223d9dc2 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/index.ts @@ -0,0 +1,24 @@ +/* + * 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('Services', () => { + loadTestFile(require.resolve('./error_groups/error_groups_detailed_statistics.spec.ts')); + loadTestFile(require.resolve('./error_groups/error_groups_main_statistics.spec.ts')); + loadTestFile(require.resolve('./service_details/service_details.spec.ts')); + loadTestFile(require.resolve('./service_icons/service_icons.spec.ts')); + loadTestFile(require.resolve('./archive_services_detailed_statistics.spec.ts')); + loadTestFile(require.resolve('./derived_annotations.spec.ts')); + loadTestFile(require.resolve('./get_service_node_metadata.spec.ts')); + loadTestFile(require.resolve('./service_alerts.spec.ts')); + loadTestFile(require.resolve('./services_detailed_statistics.spec.ts')); + loadTestFile(require.resolve('./top_services.spec.ts')); + loadTestFile(require.resolve('./transaction_types.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_alerts.spec.ts similarity index 63% rename from x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_alerts.spec.ts index e3324546c84d5..09d9176a03a0a 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_alerts.spec.ts @@ -8,23 +8,25 @@ import expect from '@kbn/expect'; import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { ApmRuleType } from '@kbn/rule-data-utils'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { waitForAlertsForRule } from '../alerts/helpers/wait_for_alerts_for_rule'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createApmRule, runRuleSoon, ApmAlertFields } from '../alerts/helpers/alerting_api_helper'; -import { waitForActiveRule } from '../alerts/helpers/wait_for_active_rule'; -import { cleanupRuleAndAlertState } from '../alerts/helpers/cleanup_rule_and_alert_state'; - -export default function ServiceAlerts({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const supertest = getService('supertest'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - const es = getService('es'); +import type { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { + APM_ACTION_VARIABLE_INDEX, + APM_ALERTS_INDEX, + ApmAlertFields, +} from '../alerts/helpers/alerting_helper'; + +export default function ServiceAlerts({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const alertingApi = getService('alertingApi'); + const samlAuth = getService('samlAuth'); + const synthtrace = getService('synthtrace'); + const dayInMs = 24 * 60 * 60 * 1000; const start = Date.now() - dayInMs; const end = Date.now() + dayInMs; const goService = 'synth-go'; - const logger = getService('log'); async function getServiceAlerts({ serviceName, @@ -46,27 +48,33 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { }); } - function createRule() { - return createApmRule({ - supertest, - name: `Latency threshold | ${goService}`, - params: { - serviceName: goService, - transactionType: undefined, - windowSize: 5, - windowUnit: 'h', - threshold: 100, - aggregationType: AggregationType.Avg, - environment: 'testing', - groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'], - }, - ruleTypeId: ApmRuleType.TransactionDuration, - }); - } + describe('Service alerts', () => { + let roleAuthc: RoleCredentials; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + function createRule() { + return alertingApi.createRule({ + name: `Latency threshold | ${goService}`, + params: { + serviceName: goService, + transactionType: undefined, + windowSize: 5, + windowUnit: 'h', + threshold: 100, + aggregationType: AggregationType.Avg, + environment: 'testing', + groupBy: ['service.name', 'service.environment', 'transaction.type', 'transaction.name'], + }, + ruleTypeId: ApmRuleType.TransactionDuration, + roleAuthc, + consumer: 'apm', + }); + } - // FLAKY: https://github.com/elastic/kibana/issues/177512 - registry.when('Service alerts', { config: 'basic', archives: [] }, () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + const synthServices = [ apm .service({ name: goService, environment: 'testing', agentName: 'go' }) @@ -115,6 +123,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { after(async () => { await apmSynthtraceEsClient.clean(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('with alerts', () => { @@ -124,20 +133,35 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { before(async () => { const createdRule = await createRule(); ruleId = createdRule.id; - alerts = await waitForAlertsForRule({ es, ruleId }); + alerts = ( + await alertingApi.waitForDocumentInIndex({ + indexName: APM_ALERTS_INDEX, + ruleId, + }) + ).hits.hits.map((hit) => hit._source) as ApmAlertFields[]; }); after(async () => { - await cleanupRuleAndAlertState({ es, supertest, logger }); + await alertingApi.cleanUpAlerts({ + roleAuthc, + ruleId, + alertIndexName: APM_ALERTS_INDEX, + connectorIndexName: APM_ACTION_VARIABLE_INDEX, + consumer: 'apm', + }); }); it('checks if rule is active', async () => { - const ruleStatus = await waitForActiveRule({ ruleId, supertest }); + const ruleStatus = await alertingApi.waitForRuleStatus({ + roleAuthc, + ruleId, + expectedStatus: 'active', + }); expect(ruleStatus).to.be('active'); }); it('should successfully run the rule', async () => { - const response = await runRuleSoon({ ruleId, supertest }); + const response = await alertingApi.runRule(roleAuthc, ruleId); expect(response.status).to.be(204); }); diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts new file mode 100644 index 0000000000000..8fb9195bb6800 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_details.spec.ts @@ -0,0 +1,120 @@ +/* + * 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { dataConfig, generateData } from './generate_data'; + +type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const { + service: { name: serviceName }, + } = dataConfig; + 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() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'production', + }, + }, + }); + } + + describe('Service details', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body).to.empty(); + }); + }); + + describe('when data is generated', () => { + let body: ServiceDetails; + let status: number; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start, end }); + const response = await callApi(); + body = response.body; + status = response.status; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); + + it('returns correct cloud details', () => { + const { cloud } = dataConfig; + const { + provider, + availabilityZone, + region, + machineType, + projectName, + serviceName: cloudServiceName, + } = cloud; + + expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone); + expect(first(body?.cloud?.machineTypes)).to.be(machineType); + expect(body?.cloud?.provider).to.be(provider); + expect(body?.cloud?.projectName).to.be(projectName); + expect(body?.cloud?.serviceName).to.be(cloudServiceName); + expect(first(body?.cloud?.regions)).to.be(region); + }); + + it('returns correct container details', () => { + expect(body?.container?.totalNumberInstances).to.be(1); + }); + + it('returns correct serverless details', () => { + const { cloud, serverless } = dataConfig; + const { serviceName: cloudServiceName } = cloud; + const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless; + + expect(body?.serverless?.type).to.be(cloudServiceName); + expect(body?.serverless?.functionNames).to.have.length(2); + expect(body?.serverless?.functionNames).to.contain(firstFunctionName); + expect(body?.serverless?.functionNames).to.contain(secondFunctionName); + expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType); + }); + + it('returns correct service details', () => { + const { service } = dataConfig; + const { version, runtime, framework, agent } = service; + const { name: runTimeName, version: runTimeVersion } = runtime; + const { name: agentName, version: agentVersion } = agent; + + expect(body?.service?.framework).to.be(framework); + expect(body?.service?.agent.name).to.be(agentName); + expect(body?.service?.agent.version).to.be(agentVersion); + expect(body?.service?.runtime?.name).to.be(runTimeName); + expect(body?.service?.runtime?.version).to.be(runTimeVersion); + expect(first(body?.service?.versions)).to.be(version); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/service_infra_metrics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_infra_metrics.spec.ts similarity index 89% rename from x-pack/test/apm_api_integration/tests/services/service_details/service_infra_metrics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_infra_metrics.spec.ts index 194b266e927e8..92a8412961749 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_details/service_infra_metrics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_details/service_infra_metrics.spec.ts @@ -7,25 +7,31 @@ import expect from '@kbn/expect'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata'; +import archives_metadata from '../../../../../../../apm_api_integration/common/fixtures/es_archiver/archives_metadata'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../../constants/archiver'; type ServiceOverviewInstanceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; -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 archiveName = 'infra_metrics_and_apm'; + const esArchiver = getService('esArchiver'); const { start, end } = archives_metadata[archiveName]; - registry.when( - 'When data is loaded', - { config: 'basic', archives: ['infra_metrics_and_apm'] }, - () => { + describe('Service infra metrics', () => { + describe('When data is loaded', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES[archiveName]); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES[archiveName]); + }); + describe('fetch service instance', () => { it('handles empty infra metrics data for a service node', async () => { const response = await apmApiClient.readUser({ @@ -169,6 +175,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(body.kubernetes?.replicasets).to.eql([]); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts new file mode 100644 index 0000000000000..635c6079f9c18 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/service_icons/service_icons.spec.ts @@ -0,0 +1,80 @@ +/* + * 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { getServerlessTypeFromCloudData } from '@kbn/apm-plugin/common/serverless'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { dataConfig, generateData } from './generate_data'; + +type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const { serviceName } = dataConfig; + 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() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } + + describe('Service icons', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body).to.empty(); + }); + }); + + describe('when data is generated', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let body: ServiceIconMetadata; + let status: number; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start, end }); + const response = await callApi(); + body = response.body; + status = response.status; + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns correct HTTP status', () => { + expect(status).to.be(200); + }); + + it('returns correct metadata', () => { + const { agentName, cloud } = dataConfig; + const { provider, serviceName: cloudServiceName, provider: cloudProvider } = cloud; + + expect(body.agentName).to.be(agentName); + expect(body.cloudProvider).to.be(provider); + expect(body.containerType).to.be('Kubernetes'); + expect(body.serverlessType).to.be( + getServerlessTypeFromCloudData(cloudProvider, cloudServiceName) + ); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/services_detailed_statistics.spec.ts similarity index 87% rename from x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/services_detailed_statistics.spec.ts index 0a33450e7f980..17302aed8b653 100644 --- a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/services_detailed_statistics.spec.ts @@ -13,55 +13,21 @@ import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { uniq, map } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; type ServicesDetailedStatisticsReturn = APIReturnType<'POST /internal/apm/services/detailed_statistics'>; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - - const apmApiClient = getService('apmApiClient'); - - const synthtrace = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = '2021-01-01T00:00:00.000Z'; const end = '2021-01-01T00:59:59.999Z'; const serviceNames = ['my-service']; - registry.when( - 'Services detailed statistics when data is generated', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `POST /internal/apm/services/detailed_statistics`, - params: { - query: { - start, - end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - _inspect: true, - }, - body: { - serviceNames: JSON.stringify(serviceNames), - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.be.empty(); - expect(response.body.previousPeriod).to.be.empty(); - }); - } - ); - async function getStats( overrides?: Partial< APIClientRequestParamsOf<'POST /internal/apm/services/detailed_statistics'>['params']['query'] @@ -90,12 +56,38 @@ export default function ApiTest({ getService }: FtrProviderContext) { return response.body; } - // FLAKY: https://github.com/elastic/kibana/issues/177511 - registry.when( - 'Services detailed statistics when data is generated', - { config: 'basic', archives: [] }, - () => { + describe('Services detailed statistics', () => { + describe('when data is not generated', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `POST /internal/apm/services/detailed_statistics`, + params: { + query: { + start, + end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + _inspect: true, + }, + body: { + serviceNames: JSON.stringify(serviceNames), + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.be.empty(); + expect(response.body.previousPeriod).to.be.empty(); + }); + }); + + describe('when data is generated', () => { let servicesDetailedStatistics: ServicesDetailedStatisticsReturn; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; const instance = apm.service('my-service', 'production', 'java').instance('instance'); @@ -103,12 +95,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { const EXPECTED_LATENCY = 1000; const EXPECTED_FAILURE_RATE = 0.25; + function checkStats() { + const stats = servicesDetailedStatistics.currentPeriod['my-service']; + + expect(stats).not.empty(); + + expect(uniq(map(stats.throughput, 'y'))).eql([EXPECTED_TPM], 'tpm'); + + expect(uniq(map(stats.latency, 'y'))).eql([EXPECTED_LATENCY * 1000], 'latency'); + + expect(uniq(map(stats.transactionErrorRate, 'y'))).eql( + [EXPECTED_FAILURE_RATE], + 'errorRate' + ); + } + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval( '1m' ); - await synthtrace.index([ + await apmSynthtraceEsClient.index([ interval.rate(3).generator((timestamp) => { return instance .transaction('GET /api') @@ -133,22 +141,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtrace.clean()); - - function checkStats() { - const stats = servicesDetailedStatistics.currentPeriod['my-service']; - - expect(stats).not.empty(); - - expect(uniq(map(stats.throughput, 'y'))).eql([EXPECTED_TPM], 'tpm'); - - expect(uniq(map(stats.latency, 'y'))).eql([EXPECTED_LATENCY * 1000], 'latency'); - - expect(uniq(map(stats.transactionErrorRate, 'y'))).eql( - [EXPECTED_FAILURE_RATE], - 'errorRate' - ); - } + after(() => apmSynthtraceEsClient.clean()); describe('and transaction metrics are used', () => { before(async () => { @@ -184,6 +177,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { checkStats(); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts new file mode 100644 index 0000000000000..d3183916b593b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/throughput.spec.ts @@ -0,0 +1,541 @@ +/* + * 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 { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { buildQueryFromFilters } from '@kbn/es-query'; +import { first, last, meanBy } from 'lodash'; +import moment from 'moment'; +import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +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 type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; + +type ThroughputReturn = APIReturnType<'GET /internal/apm/services/{serviceName}/throughput'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + 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( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/throughput'>['params'] + >, + processorEvent: 'transaction' | 'metric' = 'metric' + ) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'synth-go', + ...overrides?.path, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + ...(processorEvent === 'metric' + ? { + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + } + : { + documentType: ApmDocumentType.TransactionEvent, + rollupInterval: RollupInterval.None, + bucketSizeInSeconds: 30, + }), + }, + }, + }); + return response; + } + describe('Throughput when data is not loaded', () => { + describe('Twhen data is not loaded', () => { + it('handles the empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.currentPeriod.length).to.be(0); + expect(response.body.previousPeriod.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + describe('Throughput chart api', () => { + const GO_PROD_RATE = 50; + const GO_DEV_RATE = 5; + const JAVA_PROD_RATE = 45; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const serviceGoProdInstance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); + const serviceGoDevInstance = apm + .service({ name: serviceName, environment: 'development', agentName: 'go' }) + .instance('instance-b'); + + const serviceJavaInstance = apm + .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) + .instance('instance-c'); + + await apmSynthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .generator((timestamp) => + serviceGoProdInstance + .transaction({ transactionName: 'GET /api/product/list' }) + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .generator((timestamp) => + serviceGoDevInstance + .transaction({ transactionName: 'GET /api/product/:id' }) + .duration(1000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(JAVA_PROD_RATE) + .generator((timestamp) => + serviceJavaInstance + .transaction({ transactionName: 'POST /api/product/buy' }) + .duration(1000) + .timestamp(timestamp) + ), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + + describe('compare transactions and metrics based throughput', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi({}, 'metric'), + callApi({}, 'transaction'), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('production environment', () => { + let throughput: ThroughputReturn; + + before(async () => { + const throughputResponse = await callApi({ query: { environment: 'production' } }); + throughput = throughputResponse.body; + }); + + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns correct average throughput', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); + }); + }); + + describe('when synth-java is selected', () => { + let throughput: ThroughputReturn; + + before(async () => { + const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); + throughput = throughputResponse.body; + }); + + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns throughput related to java agent', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); + }); + }); + + describe('time comparisons', () => { + let throughputResponse: ThroughputReturn; + + before(async () => { + const response = await callApi({ + query: { + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + offset: '7m', + }, + }); + throughputResponse = response.body; + }); + + it('returns some data', () => { + expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); + expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); + + const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => + isFiniteNumber(y) + ); + const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => + isFiniteNumber(y) + ); + + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); + + it('has same start time for both periods', () => { + expect(first(throughputResponse.currentPeriod)?.x).to.equal( + first(throughputResponse.previousPeriod)?.x + ); + }); + + it('has same end time for both periods', () => { + expect(last(throughputResponse.currentPeriod)?.x).to.equal( + last(throughputResponse.previousPeriod)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + expect(throughputResponse.currentPeriod.length).to.be( + throughputResponse.previousPeriod.length + ); + }); + + it('has same mean value for both periods', () => { + const currentPeriodMean = meanBy( + throughputResponse.currentPeriod.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + const previousPeriodMean = meanBy( + throughputResponse.previousPeriod.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + const currentPeriod = throughputResponse.currentPeriod; + const bucketSize = currentPeriod[1].x - currentPeriod[0].x; + const durationAsMinutes = bucketSize / 1000 / 60; + [currentPeriodMean, previousPeriodMean].every((value) => + expect(roundNumber(value)).to.be.equal( + roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) + ) + ); + }); + }); + + describe('handles kuery', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi( + { + query: { + kuery: 'transaction.name : "GET /api/product/list"', + }, + }, + 'metric' + ), + callApi( + { + query: { + kuery: 'transaction.name : "GET /api/product/list"', + }, + }, + 'transaction' + ), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('handles filters', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + const filters = [ + { + meta: { + disabled: false, + negate: false, + alias: null, + key: 'transaction.name', + params: ['GET /api/product/list'], + type: 'phrases', + }, + query: { + bool: { + minimum_should_match: 1, + should: { + match_phrase: { + 'transaction.name': 'GET /api/product/list', + }, + }, + }, + }, + }, + ]; + const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'metric' + ), + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'transaction' + ), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('handles negate filters', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; + const filters = [ + { + meta: { + disabled: false, + negate: true, + alias: null, + key: 'transaction.name', + params: ['GET /api/product/list'], + type: 'phrases', + }, + query: { + bool: { + minimum_should_match: 1, + should: { + match_phrase: { + 'transaction.name': 'GET /api/product/list', + }, + }, + }, + }, + }, + ]; + const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); + + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'metric' + ), + callApi( + { + query: { + filters: serializedFilters, + }, + }, + 'transaction' + ), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); + + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_DEV_RATE)) + ); + }); + + it('has a bucket size of 30 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(30); + }); + + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); + }); + + describe('handles bad filters request', () => { + it('throws bad request error', async () => { + try { + await callApi({ + query: { environment: 'production', filters: '{}}' }, + }); + } catch (error) { + expect(error.res.status).to.be(400); + } + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts new file mode 100644 index 0000000000000..0aea10a68367a --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/top_services.spec.ts @@ -0,0 +1,364 @@ +/* + * 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +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 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 = '2021-10-01T00:00:00.000Z'; + const end = '2021-10-01T01:00:00.000Z'; + + describe('Top services', () => { + describe('APM Services Overview with a basic license when data is not generated', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services`, + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.items.length).to.be(0); + expect(response.body.maxCountExceeded).to.be(false); + expect(response.body.serviceOverflowCount).to.be(0); + }); + }); + + describe('APM Services Overview with a basic license when data is generated', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + let response: { + status: number; + body: APIReturnType<'GET /internal/apm/services'>; + }; + + const range = timerange(new Date(start).getTime(), new Date(end).getTime()); + const transactionInterval = range.interval('1s'); + const metricInterval = range.interval('30s'); + + const errorInterval = range.interval('5s'); + + const multipleEnvServiceProdInstance = apm + .service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' }) + .instance('multiple-env-service-production'); + + const multipleEnvServiceDevInstance = apm + .service({ name: 'multiple-env-service', environment: 'development', agentName: 'go' }) + .instance('multiple-env-service-development'); + + const metricOnlyInstance = apm + .service({ name: 'metric-only-service', environment: 'production', agentName: 'java' }) + .instance('metric-only-production'); + + const errorOnlyInstance = apm + .service({ name: 'error-only-service', environment: 'production', agentName: 'java' }) + .instance('error-only-production'); + + const config = { + multiple: { + prod: { + rps: 4, + duration: 1000, + }, + dev: { + rps: 1, + duration: 500, + }, + }, + }; + + function checkStats() { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + const totalRps = config.multiple.prod.rps + config.multiple.dev.rps; + + expect(multipleEnvService).to.eql({ + serviceName: 'multiple-env-service', + transactionType: 'request', + environments: ['production', 'development'], + agentName: 'go', + latency: + 1000 * + ((config.multiple.prod.duration * config.multiple.prod.rps + + config.multiple.dev.duration * config.multiple.dev.rps) / + totalRps), + throughput: totalRps * 60, + transactionErrorRate: + config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps), + }); + } + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + return apmSynthtraceEsClient.index([ + transactionInterval + .rate(config.multiple.prod.rps) + .generator((timestamp) => + multipleEnvServiceProdInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(config.multiple.prod.duration) + .success() + ), + transactionInterval + .rate(config.multiple.dev.rps) + .generator((timestamp) => + multipleEnvServiceDevInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(config.multiple.dev.duration) + .failure() + ), + transactionInterval + .rate(config.multiple.prod.rps) + .generator((timestamp) => + multipleEnvServiceDevInstance + .transaction({ transactionName: 'non-request', transactionType: 'rpc' }) + .timestamp(timestamp) + .duration(config.multiple.prod.duration) + .success() + ), + metricInterval.rate(1).generator((timestamp) => + metricOnlyInstance + .appMetrics({ + 'system.memory.actual.free': 1, + 'system.cpu.total.norm.pct': 1, + 'system.memory.total': 1, + 'system.process.cpu.total.norm.pct': 1, + }) + .timestamp(timestamp) + ), + errorInterval + .rate(1) + .generator((timestamp) => + errorOnlyInstance.error({ message: 'Foo' }).timestamp(timestamp) + ), + ]); + }); + + after(() => { + return apmSynthtraceEsClient.clean(); + }); + + describe('when no additional filters are applied', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns a successful response', () => { + expect(response.status).to.be(200); + }); + + it('returns the correct statistics', () => { + checkStats(); + }); + + it('returns services without transaction data', () => { + const serviceNames = response.body.items.map((item) => item.serviceName); + + expect(serviceNames).to.contain('metric-only-service'); + + expect(serviceNames).to.contain('error-only-service'); + }); + }); + + describe('when applying an environment filter', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: 'production', + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns data only for that environment', () => { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + const totalRps = config.multiple.prod.rps; + + expect(multipleEnvService).to.eql({ + serviceName: 'multiple-env-service', + transactionType: 'request', + environments: ['production'], + agentName: 'go', + latency: 1000 * ((config.multiple.prod.duration * config.multiple.prod.rps) / totalRps), + throughput: totalRps * 60, + transactionErrorRate: 0, + }); + }); + }); + + describe('when applying a kuery filter', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: 'service.node.name:"multiple-env-service-development"', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns data for that kuery filter only', () => { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + const totalRps = config.multiple.dev.rps; + + expect(multipleEnvService).to.eql({ + serviceName: 'multiple-env-service', + transactionType: 'request', + environments: ['development'], + agentName: 'go', + latency: 1000 * ((config.multiple.dev.duration * config.multiple.dev.rps) / totalRps), + throughput: totalRps * 60, + transactionErrorRate: 1, + }); + }); + }); + + describe('when excluding default transaction types', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: 'not (transaction.type:request)', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns data for the top transaction type that is not a default', () => { + const multipleEnvService = response.body.items.find( + (item) => item.serviceName === 'multiple-env-service' + ); + + expect(multipleEnvService?.transactionType).to.eql('rpc'); + }); + }); + + describe('when using service transaction metrics', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.OneMinute, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns services without transaction data', () => { + const serviceNames = response.body.items.map((item) => item.serviceName); + + expect(serviceNames).to.contain('metric-only-service'); + + expect(serviceNames).to.contain('error-only-service'); + }); + + it('returns the correct statistics', () => { + checkStats(); + }); + }); + + describe('when using rolled up data', () => { + before(async () => { + response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + start, + end, + environment: ENVIRONMENT_ALL.value, + kuery: '', + probability: 1, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.TenMinutes, + useDurationSummary: true, + }, + }, + }); + }); + + it('returns the correct statistics', () => { + checkStats(); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.ts new file mode 100644 index 0000000000000..b76f05f60631b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/transaction_types.spec.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 expect from '@kbn/expect'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import { 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 = '2023-10-28T00:00:00.000Z'; + const end = '2023-10-28T00:14:59.999Z'; + + const serviceName = 'opbeans-node'; + + async function getTransactionTypes() { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', + params: { + path: { serviceName }, + query: { + start, + end, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + }, + }, + }); + + return response; + } + + describe('Transaction types', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getTransactionTypes(); + + expect(response.status).to.be(200); + + expect(response.body.transactionTypes.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval( + '1m' + ); + + const instance = apm.service(serviceName, 'production', 'node').instance('instance'); + + await apmSynthtraceEsClient.index([ + interval.rate(3).generator((timestamp) => { + return instance + .transaction({ transactionName: 'GET /api', transactionType: 'request' }) + .duration(1000) + .outcome('success') + .timestamp(timestamp); + }), + interval.rate(1).generator((timestamp) => { + return instance + .transaction({ transactionName: 'rm -rf *', transactionType: 'worker' }) + .duration(100) + .outcome('failure') + .timestamp(timestamp); + }), + ]); + }); + + after(() => apmSynthtraceEsClient.clean()); + it('displays available tx types', async () => { + const response = await getTransactionTypes(); + + expect(response.status).to.be(200); + expect(response.body.transactionTypes.length).to.be.greaterThan(0); + + expect(response.body.transactionTypes).to.eql(['request', 'worker']); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts index dd09804b5da83..ee1047d6024ca 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts @@ -1099,6 +1099,25 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide return body; }, + async runRule(roleAuthc: RoleCredentials, ruleId: string) { + return await retry.tryForTime(retryTimeout, async () => { + try { + const response = await supertestWithoutAuth + .post(`/internal/alerting/rule/${ruleId}/_run_soon`) + .set(samlAuth.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + + if (response.status !== 204) { + throw new Error(`runRuleSoon got ${response.status} status`); + } + return response; + } catch (error) { + throw new Error(`[Rule] Running a rule ${ruleId} failed: ${error}`); + } + }); + }, + async findInRules(roleAuthc: RoleCredentials, ruleId: string) { const response = await supertestWithoutAuth .get('/api/alerting/rules/_find') diff --git a/x-pack/test/apm_api_integration/tests/services/agent.spec.ts b/x-pack/test/apm_api_integration/tests/services/agent.spec.ts deleted file mode 100644 index 69f1938192b9e..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/agent.spec.ts +++ /dev/null @@ -1,59 +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 { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - registry.when('Agent name when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/agent', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start, - end, - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); - }); - - registry.when( - 'Agent name when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the agent name', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/agent', - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start, - end, - }, - }, - }); - - expect(response.status).to.be(200); - - expect(response.body).to.eql({ agentName: 'nodejs', runtimeName: 'node' }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts index 8fed930dec356..61feba7b15d42 100644 --- a/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/archive_services_detailed_statistics.spec.ts @@ -27,38 +27,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { start, end } = metadata; const serviceNames = ['opbeans-java', 'opbeans-go']; - registry.when( - 'Services detailed statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `POST /internal/apm/services/detailed_statistics`, - params: { - query: { - start, - end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - offset: '1d', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - }, - body: { - serviceNames: JSON.stringify(serviceNames), - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.be.empty(); - expect(response.body.previousPeriod).to.be.empty(); - }); - } - ); - registry.when( 'Services detailed statistics when data is loaded', { config: 'basic', archives: [archiveName] }, diff --git a/x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts b/x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts deleted file mode 100644 index 6fe079cba963e..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/derived_annotations.spec.ts +++ /dev/null @@ -1,171 +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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; - -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function annotationApiTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const es = getService('es'); - - const dates = [ - new Date('2021-02-01T00:00:00.000Z'), - new Date('2021-02-01T01:00:00.000Z'), - new Date('2021-02-01T02:00:00.000Z'), - new Date('2021-02-01T03:00:00.000Z'), - ]; - - const indexName = 'apm-8.0.0-transaction'; - - registry.when( - 'Derived deployment annotations with a basic license', - { config: 'basic', archives: [] }, - () => { - describe('when there are multiple service versions', () => { - let response: APIReturnType<'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31'>; - - before(async () => { - const indexExists = await es.indices.exists({ index: indexName }); - if (indexExists) { - await es.indices.delete({ - index: indexName, - }); - } - - await es.indices.create({ - index: indexName, - body: { - mappings: { - properties: { - service: { - properties: { - name: { - type: 'keyword', - }, - version: { - type: 'keyword', - }, - environment: { - type: 'keyword', - }, - }, - }, - transaction: { - properties: { - type: { - type: 'keyword', - }, - duration: { - type: 'long', - }, - }, - }, - processor: { - properties: { - event: { - type: 'keyword', - }, - }, - }, - }, - }, - }, - }); - - const docs = dates.flatMap((date, index) => { - const baseAnnotation = { - transaction: { - type: 'request', - duration: 1000000, - }, - - service: { - name: 'opbeans-java', - environment: 'production', - version: index + 1, - }, - processor: { - event: 'transaction', - }, - }; - return [ - { - ...baseAnnotation, - '@timestamp': date.toISOString(), - }, - { - ...baseAnnotation, - '@timestamp': new Date(date.getTime() + 30000), - }, - { - ...baseAnnotation, - '@timestamp': new Date(date.getTime() + 60000), - }, - ]; - }); - - await es.bulk({ - index: indexName, - body: docs.flatMap((doc) => [{ index: {} }, doc]), - refresh: true, - }); - - response = ( - await apmApiClient.readUser({ - endpoint: 'GET /api/apm/services/{serviceName}/annotation/search 2023-10-31', - params: { - path: { - serviceName: 'opbeans-java', - }, - query: { - start: dates[1].toISOString(), - end: dates[2].toISOString(), - environment: 'production', - }, - }, - }) - ).body; - }); - - it('annotations are displayed for the service versions in the given time range', async () => { - expect(response.annotations.length).to.be(2); - expect(response.annotations[0]['@timestamp']).to.be(dates[1].getTime()); - expect(response.annotations[1]['@timestamp']).to.be(dates[2].getTime()); - - expectSnapshot(response.annotations[0]).toMatchInline(` - Object { - "@timestamp": 1612141200000, - "id": "2", - "text": "2", - "type": "version", - } - `); - }); - - it('annotations are not displayed for the service versions outside of the given time range', () => { - expect( - response.annotations.some((annotation) => { - return ( - annotation['@timestamp'] !== dates[0].getTime() && - annotation['@timestamp'] !== dates[2].getTime() - ); - }) - ); - }); - - after(async () => { - await es.indices.delete({ - index: indexName, - }); - }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts deleted file mode 100644 index 4d8b250ec623c..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts +++ /dev/null @@ -1,121 +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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { dataConfig, generateData } from './generate_data'; - -type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const { - service: { name: serviceName }, - } = dataConfig; - 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() { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details', - params: { - path: { serviceName }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'production', - }, - }, - }); - } - - registry.when( - 'Service details when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - expect(body).to.empty(); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177663 - registry.when('Service details when data is generated', { config: 'basic', archives: [] }, () => { - let body: ServiceDetails; - let status: number; - - before(async () => { - await generateData({ apmSynthtraceEsClient, start, end }); - const response = await callApi(); - body = response.body; - status = response.status; - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns correct HTTP status', () => { - expect(status).to.be(200); - }); - - it('returns correct cloud details', () => { - const { cloud } = dataConfig; - const { - provider, - availabilityZone, - region, - machineType, - projectName, - serviceName: cloudServiceName, - } = cloud; - - expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone); - expect(first(body?.cloud?.machineTypes)).to.be(machineType); - expect(body?.cloud?.provider).to.be(provider); - expect(body?.cloud?.projectName).to.be(projectName); - expect(body?.cloud?.serviceName).to.be(cloudServiceName); - expect(first(body?.cloud?.regions)).to.be(region); - }); - - it('returns correct container details', () => { - expect(body?.container?.totalNumberInstances).to.be(1); - }); - - it('returns correct serverless details', () => { - const { cloud, serverless } = dataConfig; - const { serviceName: cloudServiceName } = cloud; - const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless; - - expect(body?.serverless?.type).to.be(cloudServiceName); - expect(body?.serverless?.functionNames).to.have.length(2); - expect(body?.serverless?.functionNames).to.contain(firstFunctionName); - expect(body?.serverless?.functionNames).to.contain(secondFunctionName); - expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType); - }); - - it('returns correct service details', () => { - const { service } = dataConfig; - const { version, runtime, framework, agent } = service; - const { name: runTimeName, version: runTimeVersion } = runtime; - const { name: agentName, version: agentVersion } = agent; - - expect(body?.service?.framework).to.be(framework); - expect(body?.service?.agent.name).to.be(agentName); - expect(body?.service?.agent.version).to.be(agentVersion); - expect(body?.service?.runtime?.name).to.be(runTimeName); - expect(body?.service?.runtime?.version).to.be(runTimeVersion); - expect(first(body?.service?.versions)).to.be(version); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts deleted file mode 100644 index 3516edd1800cb..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts +++ /dev/null @@ -1,77 +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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { getServerlessTypeFromCloudData } from '@kbn/apm-plugin/common/serverless'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { dataConfig, generateData } from './generate_data'; - -type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const { serviceName } = dataConfig; - 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() { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons', - params: { - path: { serviceName }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - }, - }, - }); - } - - registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - expect(body).to.empty(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177662 - registry.when('Service icons when data is generated', { config: 'basic', archives: [] }, () => { - let body: ServiceIconMetadata; - let status: number; - - before(async () => { - await generateData({ apmSynthtraceEsClient, start, end }); - const response = await callApi(); - body = response.body; - status = response.status; - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns correct HTTP status', () => { - expect(status).to.be(200); - }); - - it('returns correct metadata', () => { - const { agentName, cloud } = dataConfig; - const { provider, serviceName: cloudServiceName, provider: cloudProvider } = cloud; - - expect(body.agentName).to.be(agentName); - expect(body.cloudProvider).to.be(provider); - expect(body.containerType).to.be('Kubernetes'); - expect(body.serverlessType).to.be( - getServerlessTypeFromCloudData(cloudProvider, cloudServiceName) - ); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts deleted file mode 100644 index d8b3227890f1d..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts +++ /dev/null @@ -1,535 +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 { apm, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { buildQueryFromFilters } from '@kbn/es-query'; -import { first, last, meanBy } from 'lodash'; -import moment from 'moment'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -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 { roundNumber } from '../../utils'; - -type ThroughputReturn = APIReturnType<'GET /internal/apm/services/{serviceName}/throughput'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const serviceName = 'synth-go'; - 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( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/throughput'>['params'] - >, - processorEvent: 'transaction' | 'metric' = 'metric' - ) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/throughput', - params: { - path: { - serviceName: 'synth-go', - ...overrides?.path, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - ...(processorEvent === 'metric' - ? { - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - } - : { - documentType: ApmDocumentType.TransactionEvent, - rollupInterval: RollupInterval.None, - bucketSizeInSeconds: 30, - }), - }, - }, - }); - return response; - } - - registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await callApi(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod.length).to.be(0); - expect(response.body.previousPeriod.length).to.be(0); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/177510 - registry.when('Throughput when data is loaded', { config: 'basic', archives: [] }, () => { - describe('Throughput chart api', () => { - const GO_PROD_RATE = 50; - const GO_DEV_RATE = 5; - const JAVA_PROD_RATE = 45; - - before(async () => { - const serviceGoProdInstance = apm - .service({ name: serviceName, environment: 'production', agentName: 'go' }) - .instance('instance-a'); - const serviceGoDevInstance = apm - .service({ name: serviceName, environment: 'development', agentName: 'go' }) - .instance('instance-b'); - - const serviceJavaInstance = apm - .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) - .instance('instance-c'); - - await apmSynthtraceEsClient.index([ - timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .generator((timestamp) => - serviceGoProdInstance - .transaction({ transactionName: 'GET /api/product/list' }) - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .generator((timestamp) => - serviceGoDevInstance - .transaction({ transactionName: 'GET /api/product/:id' }) - .duration(1000) - .timestamp(timestamp) - ), - timerange(start, end) - .interval('1m') - .rate(JAVA_PROD_RATE) - .generator((timestamp) => - serviceJavaInstance - .transaction({ transactionName: 'POST /api/product/buy' }) - .duration(1000) - .timestamp(timestamp) - ), - ]); - }); - - after(() => apmSynthtraceEsClient.clean()); - - describe('compare transactions and metrics based throughput', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi({}, 'metric'), - callApi({}, 'transaction'), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('production environment', () => { - let throughput: ThroughputReturn; - - before(async () => { - const throughputResponse = await callApi({ query: { environment: 'production' } }); - throughput = throughputResponse.body; - }); - - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns correct average throughput', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); - }); - }); - - describe('when synth-java is selected', () => { - let throughput: ThroughputReturn; - - before(async () => { - const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); - throughput = throughputResponse.body; - }); - - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns throughput related to java agent', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); - }); - }); - - describe('time comparisons', () => { - let throughputResponse: ThroughputReturn; - - before(async () => { - const response = await callApi({ - query: { - start: moment(end).subtract(7, 'minutes').toISOString(), - end: new Date(end).toISOString(), - offset: '7m', - }, - }); - throughputResponse = response.body; - }); - - it('returns some data', () => { - expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); - expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); - - const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => - isFiniteNumber(y) - ); - const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => - isFiniteNumber(y) - ); - - expect(hasCurrentPeriodData).to.equal(true); - expect(hasPreviousPeriodData).to.equal(true); - }); - - it('has same start time for both periods', () => { - expect(first(throughputResponse.currentPeriod)?.x).to.equal( - first(throughputResponse.previousPeriod)?.x - ); - }); - - it('has same end time for both periods', () => { - expect(last(throughputResponse.currentPeriod)?.x).to.equal( - last(throughputResponse.previousPeriod)?.x - ); - }); - - it('returns same number of buckets for both periods', () => { - expect(throughputResponse.currentPeriod.length).to.be( - throughputResponse.previousPeriod.length - ); - }); - - it('has same mean value for both periods', () => { - const currentPeriodMean = meanBy( - throughputResponse.currentPeriod.filter((item) => isFiniteNumber(item.y) && item.y > 0), - 'y' - ); - const previousPeriodMean = meanBy( - throughputResponse.previousPeriod.filter( - (item) => isFiniteNumber(item.y) && item.y > 0 - ), - 'y' - ); - const currentPeriod = throughputResponse.currentPeriod; - const bucketSize = currentPeriod[1].x - currentPeriod[0].x; - const durationAsMinutes = bucketSize / 1000 / 60; - [currentPeriodMean, previousPeriodMean].every((value) => - expect(roundNumber(value)).to.be.equal( - roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) - ) - ); - }); - }); - - describe('handles kuery', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi( - { - query: { - kuery: 'transaction.name : "GET /api/product/list"', - }, - }, - 'metric' - ), - callApi( - { - query: { - kuery: 'transaction.name : "GET /api/product/list"', - }, - }, - 'transaction' - ), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('handles filters', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - const filters = [ - { - meta: { - disabled: false, - negate: false, - alias: null, - key: 'transaction.name', - params: ['GET /api/product/list'], - type: 'phrases', - }, - query: { - bool: { - minimum_should_match: 1, - should: { - match_phrase: { - 'transaction.name': 'GET /api/product/list', - }, - }, - }, - }, - }, - ]; - const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'metric' - ), - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'transaction' - ), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('handles negate filters', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; - const filters = [ - { - meta: { - disabled: false, - negate: true, - alias: null, - key: 'transaction.name', - params: ['GET /api/product/list'], - type: 'phrases', - }, - query: { - bool: { - minimum_should_match: 1, - should: { - match_phrase: { - 'transaction.name': 'GET /api/product/list', - }, - }, - }, - }, - }, - ]; - const serializedFilters = JSON.stringify(buildQueryFromFilters(filters, undefined)); - - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'metric' - ), - callApi( - { - query: { - filters: serializedFilters, - }, - }, - 'transaction' - ), - ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; - }); - - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); - - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_DEV_RATE)) - ); - }); - - it('has a bucket size of 30 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(30); - }); - - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); - - describe('handles bad filters request', () => { - it('throws bad request error', async () => { - try { - await callApi({ - query: { environment: 'production', filters: '{}}' }, - }); - } catch (error) { - expect(error.res.status).to.be(400); - } - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts index 48b394c092638..41012d8836268 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; @@ -20,7 +19,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); const archiveName = 'apm_8.0.0'; @@ -30,355 +28,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const archiveStart = archiveRange.start; const archiveEnd = archiveRange.end; - const start = '2021-10-01T00:00:00.000Z'; - const end = '2021-10-01T01:00:00.000Z'; - - registry.when( - 'APM Services Overview with a basic license when data is not generated', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services`, - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.items.length).to.be(0); - expect(response.body.maxCountExceeded).to.be(false); - expect(response.body.serviceOverflowCount).to.be(0); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177509 - registry.when( - 'APM Services Overview with a basic license when data is generated', - { config: 'basic', archives: [] }, - () => { - let response: { - status: number; - body: APIReturnType<'GET /internal/apm/services'>; - }; - - const range = timerange(new Date(start).getTime(), new Date(end).getTime()); - const transactionInterval = range.interval('1s'); - const metricInterval = range.interval('30s'); - - const errorInterval = range.interval('5s'); - - const multipleEnvServiceProdInstance = apm - .service({ name: 'multiple-env-service', environment: 'production', agentName: 'go' }) - .instance('multiple-env-service-production'); - - const multipleEnvServiceDevInstance = apm - .service({ name: 'multiple-env-service', environment: 'development', agentName: 'go' }) - .instance('multiple-env-service-development'); - - const metricOnlyInstance = apm - .service({ name: 'metric-only-service', environment: 'production', agentName: 'java' }) - .instance('metric-only-production'); - - const errorOnlyInstance = apm - .service({ name: 'error-only-service', environment: 'production', agentName: 'java' }) - .instance('error-only-production'); - - const config = { - multiple: { - prod: { - rps: 4, - duration: 1000, - }, - dev: { - rps: 1, - duration: 500, - }, - }, - }; - - function checkStats() { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - const totalRps = config.multiple.prod.rps + config.multiple.dev.rps; - - expect(multipleEnvService).to.eql({ - serviceName: 'multiple-env-service', - transactionType: 'request', - environments: ['production', 'development'], - agentName: 'go', - latency: - 1000 * - ((config.multiple.prod.duration * config.multiple.prod.rps + - config.multiple.dev.duration * config.multiple.dev.rps) / - totalRps), - throughput: totalRps * 60, - transactionErrorRate: - config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps), - }); - } - - before(async () => { - return synthtrace.index([ - transactionInterval - .rate(config.multiple.prod.rps) - .generator((timestamp) => - multipleEnvServiceProdInstance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(config.multiple.prod.duration) - .success() - ), - transactionInterval - .rate(config.multiple.dev.rps) - .generator((timestamp) => - multipleEnvServiceDevInstance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(config.multiple.dev.duration) - .failure() - ), - transactionInterval - .rate(config.multiple.prod.rps) - .generator((timestamp) => - multipleEnvServiceDevInstance - .transaction({ transactionName: 'non-request', transactionType: 'rpc' }) - .timestamp(timestamp) - .duration(config.multiple.prod.duration) - .success() - ), - metricInterval.rate(1).generator((timestamp) => - metricOnlyInstance - .appMetrics({ - 'system.memory.actual.free': 1, - 'system.cpu.total.norm.pct': 1, - 'system.memory.total': 1, - 'system.process.cpu.total.norm.pct': 1, - }) - .timestamp(timestamp) - ), - errorInterval - .rate(1) - .generator((timestamp) => - errorOnlyInstance.error({ message: 'Foo' }).timestamp(timestamp) - ), - ]); - }); - - after(() => { - return synthtrace.clean(); - }); - - describe('when no additional filters are applied', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns a successful response', () => { - expect(response.status).to.be(200); - }); - - it('returns the correct statistics', () => { - checkStats(); - }); - - it('returns services without transaction data', () => { - const serviceNames = response.body.items.map((item) => item.serviceName); - - expect(serviceNames).to.contain('metric-only-service'); - - expect(serviceNames).to.contain('error-only-service'); - }); - }); - - describe('when applying an environment filter', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: 'production', - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns data only for that environment', () => { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - const totalRps = config.multiple.prod.rps; - - expect(multipleEnvService).to.eql({ - serviceName: 'multiple-env-service', - transactionType: 'request', - environments: ['production'], - agentName: 'go', - latency: 1000 * ((config.multiple.prod.duration * config.multiple.prod.rps) / totalRps), - throughput: totalRps * 60, - transactionErrorRate: 0, - }); - }); - }); - - describe('when applying a kuery filter', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: 'service.node.name:"multiple-env-service-development"', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns data for that kuery filter only', () => { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - const totalRps = config.multiple.dev.rps; - - expect(multipleEnvService).to.eql({ - serviceName: 'multiple-env-service', - transactionType: 'request', - environments: ['development'], - agentName: 'go', - latency: 1000 * ((config.multiple.dev.duration * config.multiple.dev.rps) / totalRps), - throughput: totalRps * 60, - transactionErrorRate: 1, - }); - }); - }); - - describe('when excluding default transaction types', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: 'not (transaction.type:request)', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns data for the top transaction type that is not a default', () => { - const multipleEnvService = response.body.items.find( - (item) => item.serviceName === 'multiple-env-service' - ); - - expect(multipleEnvService?.transactionType).to.eql('rpc'); - }); - }); - - describe('when using service transaction metrics', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.ServiceTransactionMetric, - rollupInterval: RollupInterval.OneMinute, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns services without transaction data', () => { - const serviceNames = response.body.items.map((item) => item.serviceName); - - expect(serviceNames).to.contain('metric-only-service'); - - expect(serviceNames).to.contain('error-only-service'); - }); - - it('returns the correct statistics', () => { - checkStats(); - }); - }); - - describe('when using rolled up data', () => { - before(async () => { - response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services', - params: { - query: { - start, - end, - environment: ENVIRONMENT_ALL.value, - kuery: '', - probability: 1, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.TenMinutes, - useDurationSummary: true, - }, - }, - }); - }); - - it('returns the correct statistics', () => { - checkStats(); - }); - }); - } - ); - registry.when( 'APM Services Overview with a trial license when data is loaded', { config: 'trial', archives: [archiveName] }, diff --git a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts deleted file mode 100644 index 57dbbb1c6ee49..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts +++ /dev/null @@ -1,92 +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 { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); - - const start = '2023-10-28T00:00:00.000Z'; - const end = '2023-10-28T00:14:59.999Z'; - - const serviceName = 'opbeans-node'; - - async function getTransactionTypes() { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/services/{serviceName}/transaction_types', - params: { - path: { serviceName }, - query: { - start, - end, - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - }, - }, - }); - - return response; - } - - registry.when( - 'Transaction types when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const response = await getTransactionTypes(); - - expect(response.status).to.be(200); - - expect(response.body.transactionTypes.length).to.be(0); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177521 - registry.when('Transaction types when data is loaded', { config: 'basic', archives: [] }, () => { - before(async () => { - const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval( - '1m' - ); - - const instance = apm.service(serviceName, 'production', 'node').instance('instance'); - - await synthtrace.index([ - interval.rate(3).generator((timestamp) => { - return instance - .transaction({ transactionName: 'GET /api', transactionType: 'request' }) - .duration(1000) - .outcome('success') - .timestamp(timestamp); - }), - interval.rate(1).generator((timestamp) => { - return instance - .transaction({ transactionName: 'rm -rf *', transactionType: 'worker' }) - .duration(100) - .outcome('failure') - .timestamp(timestamp); - }), - ]); - }); - - after(() => synthtrace.clean()); - it('displays available tx types', async () => { - const response = await getTransactionTypes(); - - expect(response.status).to.be(200); - expect(response.body.transactionTypes.length).to.be.greaterThan(0); - - expect(response.body.transactionTypes).to.eql(['request', 'worker']); - }); - }); -} From cffa49773c68f3c10ea1c1ccea927e35797cda51 Mon Sep 17 00:00:00 2001 From: Sid Date: Thu, 14 Nov 2024 13:37:59 +0100 Subject: [PATCH 08/23] Upgrade `cross-spawn` to v7.0.5 (#199794) Update `cross-spawn` v7.x to v7.0.5. Co-authored-by: Elastic Machine --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a6befe6cf3110..2dd177d56de3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15079,9 +15079,9 @@ cross-spawn@^6.0.0: which "^1.2.9" cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From 75679b21b176b75081a3d7eb59f432de30fbfd97 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 14 Nov 2024 07:52:13 -0500 Subject: [PATCH 09/23] [Fleet] Revert enrollment api key list removal (#200087) --- oas_docs/bundle.json | 46 ++++++++++++++++++- oas_docs/bundle.serverless.json | 46 ++++++++++++++++++- oas_docs/output/kibana.serverless.yaml | 39 ++++++++++++++++ oas_docs/output/kibana.yaml | 39 ++++++++++++++++ .../types/rest_spec/enrollment_api_key.ts | 5 +- .../routes/enrollment_api_key/handler.ts | 1 + .../server/routes/enrollment_api_key/index.ts | 5 +- .../apis/enrollment_api_keys/crud.ts | 2 + 8 files changed, 179 insertions(+), 4 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index aa53ce68e54fd..0fde92c1fa3f9 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -16951,6 +16951,49 @@ }, "type": "array" }, + "list": { + "deprecated": true, + "items": { + "additionalProperties": false, + "properties": { + "active": { + "description": "When false, the enrollment API key is revoked and cannot be used for enrolling Elastic Agents.", + "type": "boolean" + }, + "api_key": { + "description": "The enrollment API key (token) used for enrolling Elastic Agents.", + "type": "string" + }, + "api_key_id": { + "description": "The ID of the API key in the Security API.", + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "description": "The name of the enrollment API key.", + "type": "string" + }, + "policy_id": { + "description": "The ID of the agent policy the Elastic Agent will be enrolled in.", + "type": "string" + } + }, + "required": [ + "id", + "api_key_id", + "api_key", + "active", + "created_at" + ], + "type": "object" + }, + "type": "array" + }, "page": { "type": "number" }, @@ -16965,7 +17008,8 @@ "items", "total", "page", - "perPage" + "perPage", + "list" ], "type": "object" } diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 1267027a3687a..229bda84b8629 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -16951,6 +16951,49 @@ }, "type": "array" }, + "list": { + "deprecated": true, + "items": { + "additionalProperties": false, + "properties": { + "active": { + "description": "When false, the enrollment API key is revoked and cannot be used for enrolling Elastic Agents.", + "type": "boolean" + }, + "api_key": { + "description": "The enrollment API key (token) used for enrolling Elastic Agents.", + "type": "string" + }, + "api_key_id": { + "description": "The ID of the API key in the Security API.", + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "description": "The name of the enrollment API key.", + "type": "string" + }, + "policy_id": { + "description": "The ID of the agent policy the Elastic Agent will be enrolled in.", + "type": "string" + } + }, + "required": [ + "id", + "api_key_id", + "api_key", + "active", + "created_at" + ], + "type": "object" + }, + "type": "array" + }, "page": { "type": "number" }, @@ -16965,7 +17008,8 @@ "items", "total", "page", - "perPage" + "perPage", + "list" ], "type": "object" } diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 10dbdbe44e26a..32d38c3569148 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -16686,6 +16686,44 @@ paths: - active - created_at type: array + list: + deprecated: true + items: + additionalProperties: false + type: object + properties: + active: + description: >- + When false, the enrollment API key is revoked and + cannot be used for enrolling Elastic Agents. + type: boolean + api_key: + description: >- + The enrollment API key (token) used for enrolling + Elastic Agents. + type: string + api_key_id: + description: The ID of the API key in the Security API. + type: string + created_at: + type: string + id: + type: string + name: + description: The name of the enrollment API key. + type: string + policy_id: + description: >- + The ID of the agent policy the Elastic Agent will be + enrolled in. + type: string + required: + - id + - api_key_id + - api_key + - active + - created_at + type: array page: type: number perPage: @@ -16697,6 +16735,7 @@ paths: - total - page - perPage + - list '400': content: application/json; Elastic-Api-Version=2023-10-31: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index f84c98205effe..8a3d4d3634b8d 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -19470,6 +19470,44 @@ paths: - active - created_at type: array + list: + deprecated: true + items: + additionalProperties: false + type: object + properties: + active: + description: >- + When false, the enrollment API key is revoked and + cannot be used for enrolling Elastic Agents. + type: boolean + api_key: + description: >- + The enrollment API key (token) used for enrolling + Elastic Agents. + type: string + api_key_id: + description: The ID of the API key in the Security API. + type: string + created_at: + type: string + id: + type: string + name: + description: The name of the enrollment API key. + type: string + policy_id: + description: >- + The ID of the agent policy the Elastic Agent will be + enrolled in. + type: string + required: + - id + - api_key_id + - api_key + - active + - created_at + type: array page: type: number perPage: @@ -19481,6 +19519,7 @@ paths: - total - page - perPage + - list '400': content: application/json; Elastic-Api-Version=2023-10-31: diff --git a/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts b/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts index e1509d551bdef..7fa724e5079c8 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/enrollment_api_key.ts @@ -13,7 +13,10 @@ export interface GetEnrollmentAPIKeysRequest { query: ListWithKuery; } -export type GetEnrollmentAPIKeysResponse = ListResult; +export type GetEnrollmentAPIKeysResponse = ListResult & { + // deprecated in 8.x + list?: EnrollmentAPIKey[]; +}; export interface GetOneEnrollmentAPIKeyRequest { params: { diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index cfce60b0f18f5..a38f5bdadc617 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -43,6 +43,7 @@ export const getEnrollmentApiKeysHandler: RequestHandler< spaceId: useSpaceAwareness ? getCurrentNamespace(soClient) : undefined, }); const body: GetEnrollmentAPIKeysResponse = { + list: items, // deprecated items, total, page, diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts index 58538ba18f359..e593bac3180fe 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts @@ -111,7 +111,10 @@ export const registerRoutes = (router: FleetAuthzRouter) => { request: GetEnrollmentAPIKeysRequestSchema, response: { 200: { - body: () => ListResponseSchema(EnrollmentAPIKeySchema), + body: () => + ListResponseSchema(EnrollmentAPIKeySchema).extends({ + list: schema.arrayOf(EnrollmentAPIKeySchema, { meta: { deprecated: true } }), + }), }, 400: { body: genericErrorResponse, diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 24d1520d8d6f8..686532f8eae50 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -43,6 +43,8 @@ export default function (providerContext: FtrProviderContext) { expect(apiResponse.total).to.be(2); expect(apiResponse.items[0]).to.have.keys('id', 'api_key_id', 'name'); + // Deprecated property list + expect(apiResponse.list[0]).to.have.keys('id', 'api_key_id', 'name'); expect(apiResponse).to.have.keys('items'); }); From a7470dd0473259418b1af748c94cc6f51dbcd41f Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Thu, 14 Nov 2024 14:09:28 +0100 Subject: [PATCH 10/23] [Security Solution] Fix Auto-grow behaviour of Filter Page Controls (#200152) ## Summary PR : https://github.com/elastic/kibana/pull/199408 disables `auto` grow behaviour page filters controls which has been default behavior for some time. It causes issue on the page filters of Security and probably Observability as well since Page Filter controls do not grow anymore. |Correct in 8.16| Broken in 8.17/9.0.0| |---|---| |![image](https://github.com/user-attachments/assets/cc120c75-971c-44e6-bd63-65e5193cc0e5)|![image](https://github.com/user-attachments/assets/9a83a75d-70e2-4543-b9db-d0cbee24a404)| --- .../kbn-alerts-ui-shared/src/alert_filter_controls/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/constants.ts b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/constants.ts index 95606e276362b..c72808d5fceab 100644 --- a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/constants.ts +++ b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/constants.ts @@ -70,6 +70,7 @@ export const COMMON_OPTIONS_LIST_CONTROL_INPUTS: Partial Date: Thu, 14 Nov 2024 08:30:10 -0500 Subject: [PATCH 11/23] [Discover] mailto links in data view urls (#200070) Adds `mailto:` as an allowed URL Url formatters in data views. --- src/plugins/field_formats/common/converters/url.test.ts | 8 ++++++++ src/plugins/field_formats/common/converters/url.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/field_formats/common/converters/url.test.ts b/src/plugins/field_formats/common/converters/url.test.ts index 7e465829e1eb1..d1c89575b2d4d 100644 --- a/src/plugins/field_formats/common/converters/url.test.ts +++ b/src/plugins/field_formats/common/converters/url.test.ts @@ -19,6 +19,14 @@ describe('UrlFormat', () => { ); }); + test('outputs a mailto: link when URL starts with mailto:', () => { + const url = new UrlFormat({}); + + expect(url.convert('mailto:test@example.com', HTML_CONTEXT_TYPE)).toBe( + 'mailto:test@example.com' + ); + }); + test('outputs an