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 558777e459bd7..1de08a3fc3f5c 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 @@ -38,5 +38,6 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./span_links')); loadTestFile(require.resolve('./suggestions')); loadTestFile(require.resolve('./throughput')); + loadTestFile(require.resolve('./service_overview')); }); } diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/es_utils.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/es_utils.ts similarity index 97% rename from x-pack/test/apm_api_integration/tests/service_overview/dependencies/es_utils.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/es_utils.ts index 3fe59f4aeaea4..453f7a50d8aa3 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/es_utils.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/es_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { v4 as uuidv4 } from 'uuid'; export function createServiceDependencyDocs({ diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/index.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/index.spec.ts new file mode 100644 index 0000000000000..cb7f81304c3b0 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/dependencies/index.spec.ts @@ -0,0 +1,304 @@ +/* + * 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 { last, pick } from 'lodash'; +import type { ValuesType } from 'utility-types'; +import { type Node, NodeType } from '@kbn/apm-plugin/common/connections'; +import { + ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED, +} from '@kbn/apm-plugin/common/environment_filter_values'; +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { roundNumber } from '../../utils/common'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context'; +import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const es = getService('es'); + + const { start, end } = { + start: '2021-08-03T06:50:15.910Z', + end: '2021-08-03T07:20:15.910Z', + }; + + function getName(node: Node) { + return node.type === NodeType.service ? node.serviceName : node.dependencyName; + } + + describe('Service Overview', () => { + describe('Dependencies', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start, + end, + numBuckets: 20, + environment: ENVIRONMENT_ALL.value, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.serviceDependencies).to.eql([]); + }); + }); + + describe('when specific data is loaded', () => { + let response: { + status: number; + body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>; + }; + + const indices = { + metric: 'apm-dependencies-metric', + transaction: 'apm-dependencies-transaction', + span: 'apm-dependencies-span', + }; + + const startTime = new Date(start).getTime(); + const endTime = new Date(end).getTime(); + + after(async () => { + const allIndices = Object.values(indices).join(','); + const indexExists = await es.indices.exists({ index: allIndices }); + if (indexExists) { + await es.indices.delete({ + index: allIndices, + }); + } + }); + + before(async () => { + await es.indices.create({ + index: indices.metric, + body: { + mappings: apmDependenciesMapping, + }, + }); + + await es.indices.create({ + index: indices.transaction, + body: { + mappings: apmDependenciesMapping, + }, + }); + + await es.indices.create({ + index: indices.span, + body: { + mappings: apmDependenciesMapping, + }, + }); + + const docs = [ + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node:3000', + outcome: 'success', + responseTime: { + count: 2, + sum: 10, + }, + time: startTime, + to: { + service: { + name: 'opbeans-node', + }, + agentName: 'nodejs', + }, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node:3000', + outcome: 'failure', + responseTime: { + count: 1, + sum: 10, + }, + time: startTime, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'postgres', + outcome: 'success', + responseTime: { + count: 1, + sum: 3, + }, + time: startTime, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node-via-proxy', + outcome: 'success', + responseTime: { + count: 1, + sum: 1, + }, + time: endTime - 1, + to: { + service: { + name: 'opbeans-node', + }, + agentName: 'nodejs', + }, + }), + ]; + + const bulkActions = docs.reduce( + (prev, doc) => { + return [...prev, { index: { _index: indices[doc.processor.event] } }, doc]; + }, + [] as Array< + | { + index: { + _index: string; + }; + } + | ValuesType + > + ); + + await es.bulk({ + body: bulkActions, + refresh: 'wait_for', + }); + + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, + params: { + path: { serviceName: 'opbeans-java' }, + query: { + start, + end, + numBuckets: 20, + environment: ENVIRONMENT_ALL.value, + }, + }, + }); + }); + + it('returns a 200', () => { + expect(response.status).to.be(200); + }); + + it('returns two dependencies', () => { + expect(response.body.serviceDependencies.length).to.be(2); + }); + + it('returns opbeans-node as a dependency', () => { + const opbeansNode = response.body.serviceDependencies.find( + (item) => getName(item.location) === 'opbeans-node' + ); + + expect(opbeansNode !== undefined).to.be(true); + + const values = { + latency: roundNumber(opbeansNode?.currentStats.latency.value), + throughput: roundNumber(opbeansNode?.currentStats.throughput.value), + errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value), + impact: opbeansNode?.currentStats.impact, + ...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'), + }; + + const count = 4; + const sum = 21; + const errors = 1; + + expect(values).to.eql({ + agentName: 'nodejs', + environment: ENVIRONMENT_NOT_DEFINED.value, + serviceName: 'opbeans-node', + type: 'service', + errorRate: roundNumber(errors / count), + latency: roundNumber(sum / count), + throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), + impact: 100, + }); + + const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y); + const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y); + + expect(firstValue).to.be(roundNumber(20 / 3)); + expect(lastValue).to.be(1); + }); + + it('returns postgres as an external dependency', () => { + const postgres = response.body.serviceDependencies.find( + (item) => getName(item.location) === 'postgres' + ); + + expect(postgres !== undefined).to.be(true); + + const values = { + latency: roundNumber(postgres?.currentStats.latency.value), + throughput: roundNumber(postgres?.currentStats.throughput.value), + errorRate: roundNumber(postgres?.currentStats.errorRate.value), + impact: postgres?.currentStats.impact, + ...pick(postgres?.location, 'spanType', 'spanSubtype', 'dependencyName', 'type'), + }; + + const count = 1; + const sum = 3; + const errors = 0; + + expect(values).to.eql({ + spanType: 'external', + spanSubtype: 'http', + dependencyName: 'postgres', + type: 'dependency', + errorRate: roundNumber(errors / count), + latency: roundNumber(sum / count), + throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), + impact: 0, + }); + }); + }); + + // UNSUPPORTED TEST CASES - when data is loaded + // TODO: These tests should be migrated to use synthtrace: https://github.com/elastic/kibana/issues/200743 + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/get_service_node_ids.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/get_service_node_ids.ts new file mode 100644 index 0000000000000..8216c33d07140 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/get_service_node_ids.ts @@ -0,0 +1,42 @@ +/* + * 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 { take } from 'lodash'; +import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; +import type { ApmApiClient } from '../custom_dashboards/api_helper'; + +export async function getServiceNodeIds({ + apmApiClient, + start, + end, + serviceName = 'opbeans-java', + count = 1, +}: { + apmApiClient: Awaited; + start: string; + end: string; + serviceName?: string; + count?: number; +}) { + const { body } = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, + params: { + path: { serviceName }, + query: { + latencyAggregationType: LatencyAggregationType.avg, + start, + end, + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', + sortField: 'throughput', + sortDirection: 'desc', + }, + }, + }); + + return take(body.currentPeriod.map((item) => item.serviceNodeName).sort(), count); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/index.ts new file mode 100644 index 0000000000000..5ab7be1ed1ec5 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('service_overview', () => { + loadTestFile(require.resolve('./instance_details.spec.ts')); + loadTestFile(require.resolve('./instances_detailed_statistics.spec.ts')); + loadTestFile(require.resolve('./instances_main_statistics.spec.ts')); + loadTestFile(require.resolve('./dependencies/index.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instance_details.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instance_details.spec.ts new file mode 100644 index 0000000000000..94ac27b90bc5f --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instance_details.spec.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { omit } 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 { apm, timerange } from '@kbn/apm-synthtrace-client'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { getServiceNodeIds } from './get_service_node_ids'; + +type ServiceOverviewInstanceDetails = + APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = '2023-08-22T00:00:00.000Z'; + const end = '2023-08-22T01:00:00.000Z'; + + describe('Service Overview', () => { + let client: ApmSynthtraceEsClient; + + before(async () => { + client = await synthtrace.createApmSynthtraceEsClient(); + }); + + describe('Instance details', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', + params: { + path: { serviceName: 'opbeans-java', serviceNodeName: 'foo' }, + query: { + start, + end, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body).to.eql({}); + }); + }); + + describe('when data is loaded', () => { + const range = timerange(new Date(start).getTime(), new Date(end).getTime()); + + const serviceInstance = apm + .service({ name: 'service1', environment: 'production', agentName: 'go' }) + .instance('multiple-env-service-production'); + + const metricOnlyInstance = apm + .service({ name: 'service1', environment: 'production', agentName: 'java' }) + .instance('multiple-env-service-production'); + + before(() => { + return client.index([ + range + .interval('1s') + .rate(4) + .generator((timestamp) => + serviceInstance + .transaction({ transactionName: 'GET /api' }) + .timestamp(timestamp) + .duration(1000) + .success() + ), + range + .interval('30s') + .rate(1) + .generator((timestamp) => + metricOnlyInstance + .containerId('123') + .podId('234') + .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) + ), + ]); + }); + + after(async () => { + await client.clean(); + }); + + describe('fetch instance details', () => { + let response: { + status: number; + body: ServiceOverviewInstanceDetails; + }; + + let serviceNodeIds: string[]; + + before(async () => { + serviceNodeIds = await getServiceNodeIds({ + apmApiClient, + start, + end, + serviceName: 'service1', + }); + + response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', + params: { + path: { serviceName: 'service1', serviceNodeName: serviceNodeIds[0] }, + query: { + start, + end, + }, + }, + }); + }); + + it('returns the instance details', () => { + expect(response.body).to.not.eql({}); + }); + + it('return the correct data', () => { + expect(omit(response.body, '@timestamp')).to.eql({ + agent: { + name: 'java', + }, + container: { + id: '123', + }, + host: { + name: 'multiple-env-service-production', + }, + kubernetes: { + container: {}, + deployment: {}, + pod: { + uid: '234', + }, + replicaset: {}, + }, + service: { + environment: 'production', + name: 'service1', + node: { + name: 'multiple-env-service-production', + }, + }, + }); + }); + }); + }); + + describe('when data is loaded but details not found', () => { + it('handles empty state when instance id not found', async () => { + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', + params: { + path: { serviceName: 'opbeans-java', serviceNodeName: 'foo' }, + query: { + start, + end, + }, + }, + }); + expect(response.status).to.be(200); + expect(response.body).to.eql({}); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_detailed_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_detailed_statistics.spec.ts new file mode 100644 index 0000000000000..b2596ae43c956 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_detailed_statistics.spec.ts @@ -0,0 +1,56 @@ +/* + * 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 { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { getServiceNodeIds } from './get_service_node_ids'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + + const serviceName = 'opbeans-java'; + + const { start, end } = { + start: '2021-08-03T06:50:15.910Z', + end: '2021-08-03T07:20:15.910Z', + }; + + describe('Service Overview', () => { + describe('Instances detailed statistics', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', + params: { + path: { serviceName }, + query: { + latencyAggregationType: LatencyAggregationType.avg, + start, + end, + numBuckets: 20, + transactionType: 'request', + serviceNodeIds: JSON.stringify( + await getServiceNodeIds({ apmApiClient, start, end }) + ), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); + }); + }); + + // UNSUPPORTED TEST CASES - when data is loaded + // TODO: These tests should be migrated to use synthtrace: https://github.com/elastic/kibana/issues/200743 + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_main_statistics.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_main_statistics.spec.ts new file mode 100644 index 0000000000000..e0e3ba254924d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_overview/instances_main_statistics.spec.ts @@ -0,0 +1,695 @@ +/* + * 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. + */ +/* + * 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 { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { apm, Instance, timerange } from '@kbn/apm-synthtrace-client'; +import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; +import type { InstancesSortField } from '@kbn/apm-plugin/common/instances'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client'; +import { sum } from 'lodash'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; + +type ServiceOverviewInstancesMainStatistics = + APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics'>; + +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:10:00.000Z').getTime(); + + async function getServiceOverviewInstancesMainStatistics({ + serviceName, + sortField = 'throughput', + sortDirection = 'desc', + }: { + serviceName: string; + sortField?: InstancesSortField; + sortDirection?: 'asc' | 'desc'; + }) { + const { body } = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, + params: { + path: { serviceName }, + query: { + latencyAggregationType: LatencyAggregationType.avg, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + transactionType: 'request', + environment: 'production', + kuery: '', + sortField, + sortDirection, + }, + }, + }); + + return body.currentPeriod; + } + + describe('Service Overview', () => { + let client: ApmSynthtraceEsClient; + + before(async () => { + client = await synthtrace.createApmSynthtraceEsClient(); + }); + + describe('Instances main statistics', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getServiceOverviewInstancesMainStatistics({ serviceName: 'foo' }); + expect(response).to.eql({}); + }); + }); + + describe('when data is loaded', () => { + describe('Return Top 100 instances', () => { + const serviceName = 'synth-node-1'; + before(async () => { + const range = timerange(start, end); + const transactionName = 'foo'; + + const successfulTimestamps = range.interval('1m').rate(1); + const failedTimestamps = range.interval('1m').rate(1); + + const instances = [...Array(200).keys()].map((index) => + apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance(`instance-${index}`) + ); + + const instanceSpans = (instance: Instance) => { + const successfulTraceEvents = successfulTimestamps.generator((timestamp) => + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + instance + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .duration(1000) + .success() + .destination('elasticsearch') + .timestamp(timestamp), + instance + .span({ spanName: 'custom_operation', spanType: 'custom' }) + .duration(100) + .success() + .timestamp(timestamp) + ) + ); + + const failedTraceEvents = failedTimestamps.generator((timestamp) => + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instance + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + ); + + const metricsets = range + .interval('30s') + .rate(1) + .generator((timestamp) => + instance + .appMetrics({ + 'system.memory.actual.free': 800, + 'system.memory.total': 1000, + 'system.cpu.total.norm.pct': 0.6, + 'system.process.cpu.total.norm.pct': 0.7, + }) + .timestamp(timestamp) + ); + + return [successfulTraceEvents, failedTraceEvents, metricsets]; + }; + + return client.index(instances.flatMap((instance) => instanceSpans(instance))); + }); + + after(async () => { + await client.clean(); + }); + describe('fetch instances', () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; + before(async () => { + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + }); + }); + it('returns top 100 instances', () => { + expect(instancesMainStats.length).to.be(100); + }); + }); + }); + + describe('Order by error rate', () => { + const serviceName = 'synth-node-1'; + before(async () => { + const range = timerange(start, end); + const transactionName = 'foo'; + /** + * Instance A + * 90 transactions = Success + * 10 transactions = Failure + * Error rate: 10% + */ + const instanceA = apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance('instance-A'); + const instanceASuccessfulTraceEvents = range + .interval('1m') + .rate(10) + .generator((timestamp, index) => + index < 10 + ? instanceA + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceA + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + : instanceA + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .success() + ); + /** + * Instance B + * 1 transactions = Success + * 9 transactions = Failure + * Error rate: 90% + */ + const instanceB = apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance('instance-B'); + const instanceBSuccessfulTraceEvents = range + .interval('1m') + .rate(1) + .generator((timestamp, index) => + index === 0 + ? instanceB + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .success() + : instanceB + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceB + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + ); + /** + * Instance C + * 2 transactions = Success + * 8 transactions = Failure + * Error rate: 80% + */ + const instanceC = apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance('instance-C'); + const instanceCSuccessfulTraceEvents = range + .interval('1m') + .rate(1) + .generator((timestamp, index) => + index < 2 + ? instanceC + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .success() + : instanceC + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceC + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + ); + /** + * Instance D + * 0 transactions = Success + * 10 transactions = Failure + * Error rate: 100% + */ + const instanceD = apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance('instance-D'); + const instanceDSuccessfulTraceEvents = range + .interval('1m') + .rate(1) + .generator((timestamp) => + instanceD + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceD + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + ); + /** + * Instance E + * 10 transactions = Success + * 0 transactions = Failure + * Error rate: 0% + */ + const instanceE = apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance('instance-E'); + const instanceESuccessfulTraceEvents = range + .interval('1m') + .rate(1) + .generator((timestamp) => + instanceE + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .success() + ); + + return client.index([ + instanceASuccessfulTraceEvents, + instanceBSuccessfulTraceEvents, + instanceCSuccessfulTraceEvents, + instanceDSuccessfulTraceEvents, + instanceESuccessfulTraceEvents, + ]); + }); + + after(async () => { + await client.clean(); + }); + describe('sort by error rate asc', () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; + before(async () => { + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + sortField: 'errorRate', + sortDirection: 'asc', + }); + }); + it('returns instances sorted asc', () => { + expect(instancesMainStats.map((item) => roundNumber(item.errorRate))).to.eql([ + 0, 0.1, 0.8, 0.9, 1, + ]); + }); + }); + describe('sort by error rate desc', () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; + before(async () => { + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + sortField: 'errorRate', + sortDirection: 'desc', + }); + }); + it('returns instances sorted desc', () => { + expect(instancesMainStats.map((item) => roundNumber(item.errorRate))).to.eql([ + 1, 0.9, 0.8, 0.1, 0, + ]); + }); + }); + }); + + describe('with transactions and system metrics', () => { + const serviceName = 'synth-node-1'; + before(async () => { + const range = timerange(start, end); + const transactionName = 'foo'; + const instances = Array(3) + .fill(0) + .map((_, idx) => { + const index = idx + 1; + return { + instance: apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance(`instance-${index}`), + duration: index * 1000, + rate: index * 10, + errorRate: 5, + }; + }); + + return client.index( + instances.flatMap(({ instance, duration, rate, errorRate }) => { + const successfulTraceEvents = range + .interval('1m') + .rate(rate) + .generator((timestamp) => + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(duration) + .success() + ); + const failedTraceEvents = range + .interval('1m') + .rate(errorRate) + .generator((timestamp) => + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(duration) + .failure() + .errors( + instance + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + ); + const metricsets = range + .interval('30s') + .rate(1) + .generator((timestamp) => + instance + .appMetrics({ + 'system.memory.actual.free': 800, + 'system.memory.total': 1000, + 'system.cpu.total.norm.pct': 0.6, + 'system.process.cpu.total.norm.pct': 0.7, + }) + .timestamp(timestamp) + ); + return [successfulTraceEvents, failedTraceEvents, metricsets]; + }) + ); + }); + + after(async () => { + await client.clean(); + }); + + describe('test order of items', () => { + ( + [ + { + field: 'throughput', + direction: 'asc', + expectedServiceNodeNames: ['instance-1', 'instance-2', 'instance-3'], + expectedValues: [15, 25, 35], + }, + { + field: 'throughput', + direction: 'desc', + expectedServiceNodeNames: ['instance-3', 'instance-2', 'instance-1'], + expectedValues: [35, 25, 15], + }, + { + field: 'latency', + direction: 'asc', + expectedServiceNodeNames: ['instance-1', 'instance-2', 'instance-3'], + expectedValues: [1000000, 2000000, 3000000], + }, + { + field: 'latency', + direction: 'desc', + expectedServiceNodeNames: ['instance-3', 'instance-2', 'instance-1'], + expectedValues: [3000000, 2000000, 1000000], + }, + { + field: 'serviceNodeName', + direction: 'asc', + expectedServiceNodeNames: ['instance-1', 'instance-2', 'instance-3'], + }, + { + field: 'serviceNodeName', + direction: 'desc', + expectedServiceNodeNames: ['instance-3', 'instance-2', 'instance-1'], + }, + ] as Array<{ + field: InstancesSortField; + direction: 'asc' | 'desc'; + expectedServiceNodeNames: string[]; + expectedValues?: number[]; + }> + ).map(({ field, direction, expectedServiceNodeNames, expectedValues }) => + describe(`fetch instances main statistics ordered by ${field} ${direction}`, () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; + + before(async () => { + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + sortField: field, + sortDirection: direction, + }); + }); + + it('returns ordered instance main stats', () => { + expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql( + expectedServiceNodeNames + ); + if (expectedValues) { + expect( + instancesMainStats.map((item) => { + const value = item[field]; + if (typeof value === 'number') { + return roundNumber(value); + } + return value; + }) + ).to.eql(expectedValues); + } + }); + + it('returns system metrics', () => { + expect(instancesMainStats.map((item) => roundNumber(item.cpuUsage))).to.eql([ + 0.7, 0.7, 0.7, + ]); + expect(instancesMainStats.map((item) => roundNumber(item.memoryUsage))).to.eql([ + 0.2, 0.2, 0.2, + ]); + }); + }) + ); + }); + }); + + describe('with transactions only', () => { + const serviceName = 'synth-node-1'; + before(async () => { + const range = timerange(start, end); + const transactionName = 'foo'; + const instances = Array(3) + .fill(0) + .map((_, idx) => { + const index = idx + 1; + return { + instance: apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance(`instance-${index}`), + duration: index * 1000, + rate: index * 10, + errorRate: 5, + }; + }); + + return client.index( + instances.flatMap(({ instance, duration, rate, errorRate }) => { + const successfulTraceEvents = range + .interval('1m') + .rate(rate) + .generator((timestamp) => + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(duration) + .success() + ); + const failedTraceEvents = range + .interval('1m') + .rate(errorRate) + .generator((timestamp) => + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(duration) + .failure() + .errors( + instance + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ) + ); + return [successfulTraceEvents, failedTraceEvents]; + }) + ); + }); + + after(async () => { + await client.clean(); + }); + + describe(`Fetch main statistics`, () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; + + before(async () => { + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + }); + }); + + it('returns instances name', () => { + expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql([ + 'instance-3', + 'instance-2', + 'instance-1', + ]); + }); + + it('returns throughput', () => { + expect(sum(instancesMainStats.map((item) => item.throughput))).to.greaterThan(0); + }); + + it('returns latency', () => { + expect(sum(instancesMainStats.map((item) => item.latency))).to.greaterThan(0); + }); + + it('returns errorRate', () => { + expect(sum(instancesMainStats.map((item) => item.errorRate))).to.greaterThan(0); + }); + + it('does not return cpu usage', () => { + expect( + instancesMainStats + .map((item) => item.cpuUsage) + .filter((value) => value !== undefined) + ).to.eql([]); + }); + + it('does not return memory usage', () => { + expect( + instancesMainStats + .map((item) => item.memoryUsage) + .filter((value) => value !== undefined) + ).to.eql([]); + }); + }); + }); + + describe('with system metrics only', () => { + const serviceName = 'synth-node-1'; + before(async () => { + const range = timerange(start, end); + const instances = Array(3) + .fill(0) + .map((_, idx) => + apm + .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) + .instance(`instance-${idx + 1}`) + ); + + return client.index( + instances.map((instance) => { + const metricsets = range + .interval('30s') + .rate(1) + .generator((timestamp) => + instance + .appMetrics({ + 'system.memory.actual.free': 800, + 'system.memory.total': 1000, + 'system.cpu.total.norm.pct': 0.6, + 'system.process.cpu.total.norm.pct': 0.7, + }) + .timestamp(timestamp) + ); + return metricsets; + }) + ); + }); + + after(async () => { + await client.clean(); + }); + + describe(`Fetch main statistics`, () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; + + before(async () => { + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + }); + }); + + it('returns instances name', () => { + expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql([ + 'instance-1', + 'instance-2', + 'instance-3', + ]); + }); + + it('does not return throughput', () => { + expect( + instancesMainStats + .map((item) => item.throughput) + .filter((value) => value !== undefined) + ).to.eql([]); + }); + + it('does not return latency', () => { + expect( + instancesMainStats + .map((item) => item.latency) + .filter((value) => value !== undefined) + ).to.eql([]); + }); + + it('does not return errorRate', () => { + expect( + instancesMainStats + .map((item) => item.errorRate) + .filter((value) => value !== undefined) + ).to.eql([]); + }); + + it('returns cpu usage', () => { + expect(sum(instancesMainStats.map((item) => item.cpuUsage))).to.greaterThan(0); + }); + + it('returns memory usage', () => { + expect(sum(instancesMainStats.map((item) => item.memoryUsage))).to.greaterThan(0); + }); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/service_overview/__snapshots__/instance_details.spec.snap b/x-pack/test/apm_api_integration/tests/service_overview/__snapshots__/instance_details.spec.snap deleted file mode 100644 index f3fb16ec38b15..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/__snapshots__/instance_details.spec.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`APM API tests service_overview/instance_details.spec.ts basic no archive Instance details when data is loaded fetch instance details return the correct data 1`] = ` -Object { - "agent": Object { - "name": "java", - }, - "container": Object { - "id": "123", - }, - "host": Object { - "name": "multiple-env-service-production", - }, - "kubernetes": Object { - "container": Object {}, - "deployment": Object {}, - "pod": Object { - "uid": "234", - }, - "replicaset": Object {}, - }, - "service": Object { - "environment": "production", - "name": "service1", - "node": Object { - "name": "multiple-env-service-production", - }, - }, -} -`; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts index 0e841e26ddde4..fd06ee9f95266 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts @@ -6,23 +6,16 @@ */ import expect from '@kbn/expect'; -import { last, omit, pick, sortBy } from 'lodash'; -import { ValuesType } from 'utility-types'; -import { Node, NodeType } from '@kbn/apm-plugin/common/connections'; -import { - ENVIRONMENT_ALL, - ENVIRONMENT_NOT_DEFINED, -} from '@kbn/apm-plugin/common/environment_filter_values'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { roundNumber } from '../../../utils'; +import { omit, sortBy } from 'lodash'; +import { type Node, NodeType } from '@kbn/apm-plugin/common/connections'; +import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; import archives from '../../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; +import type { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const es = getService('es'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; @@ -31,278 +24,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { return node.type === NodeType.service ? node.serviceName : node.dependencyName; } - registry.when( - 'Service overview dependencies 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}/dependencies`, - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start, - end, - numBuckets: 20, - environment: ENVIRONMENT_ALL.value, - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.serviceDependencies).to.eql([]); - }); - } - ); - - registry.when( - 'Service overview dependencies when specific data is loaded', - { config: 'basic', archives: [] }, - () => { - let response: { - status: number; - body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>; - }; - - const indices = { - metric: 'apm-dependencies-metric', - transaction: 'apm-dependencies-transaction', - span: 'apm-dependencies-span', - }; - - const startTime = new Date(start).getTime(); - const endTime = new Date(end).getTime(); - - after(async () => { - const allIndices = Object.values(indices).join(','); - const indexExists = await es.indices.exists({ index: allIndices }); - if (indexExists) { - await es.indices.delete({ - index: allIndices, - }); - } - }); - - before(async () => { - await es.indices.create({ - index: indices.metric, - body: { - mappings: apmDependenciesMapping, - }, - }); - - await es.indices.create({ - index: indices.transaction, - body: { - mappings: apmDependenciesMapping, - }, - }); - - await es.indices.create({ - index: indices.span, - body: { - mappings: apmDependenciesMapping, - }, - }); - - const docs = [ - ...createServiceDependencyDocs({ - service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'opbeans-node:3000', - outcome: 'success', - responseTime: { - count: 2, - sum: 10, - }, - time: startTime, - to: { - service: { - name: 'opbeans-node', - }, - agentName: 'nodejs', - }, - }), - ...createServiceDependencyDocs({ - service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'opbeans-node:3000', - outcome: 'failure', - responseTime: { - count: 1, - sum: 10, - }, - time: startTime, - }), - ...createServiceDependencyDocs({ - service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'postgres', - outcome: 'success', - responseTime: { - count: 1, - sum: 3, - }, - time: startTime, - }), - ...createServiceDependencyDocs({ - service: { - name: 'opbeans-java', - environment: 'production', - }, - agentName: 'java', - span: { - type: 'external', - subtype: 'http', - }, - resource: 'opbeans-node-via-proxy', - outcome: 'success', - responseTime: { - count: 1, - sum: 1, - }, - time: endTime - 1, - to: { - service: { - name: 'opbeans-node', - }, - agentName: 'nodejs', - }, - }), - ]; - - const bulkActions = docs.reduce( - (prev, doc) => { - return [...prev, { index: { _index: indices[doc.processor.event] } }, doc]; - }, - [] as Array< - | { - index: { - _index: string; - }; - } - | ValuesType - > - ); - - await es.bulk({ - body: bulkActions, - refresh: 'wait_for', - }); - - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, - params: { - path: { serviceName: 'opbeans-java' }, - query: { - start, - end, - numBuckets: 20, - environment: ENVIRONMENT_ALL.value, - }, - }, - }); - }); - - it('returns a 200', () => { - expect(response.status).to.be(200); - }); - - it('returns two dependencies', () => { - expect(response.body.serviceDependencies.length).to.be(2); - }); - - it('returns opbeans-node as a dependency', () => { - const opbeansNode = response.body.serviceDependencies.find( - (item) => getName(item.location) === 'opbeans-node' - ); - - expect(opbeansNode !== undefined).to.be(true); - - const values = { - latency: roundNumber(opbeansNode?.currentStats.latency.value), - throughput: roundNumber(opbeansNode?.currentStats.throughput.value), - errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value), - impact: opbeansNode?.currentStats.impact, - ...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'), - }; - - const count = 4; - const sum = 21; - const errors = 1; - - expect(values).to.eql({ - agentName: 'nodejs', - environment: ENVIRONMENT_NOT_DEFINED.value, - serviceName: 'opbeans-node', - type: 'service', - errorRate: roundNumber(errors / count), - latency: roundNumber(sum / count), - throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), - impact: 100, - }); - - const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y); - const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y); - - expect(firstValue).to.be(roundNumber(20 / 3)); - expect(lastValue).to.be(1); - }); - - it('returns postgres as an external dependency', () => { - const postgres = response.body.serviceDependencies.find( - (item) => getName(item.location) === 'postgres' - ); - - expect(postgres !== undefined).to.be(true); - - const values = { - latency: roundNumber(postgres?.currentStats.latency.value), - throughput: roundNumber(postgres?.currentStats.throughput.value), - errorRate: roundNumber(postgres?.currentStats.errorRate.value), - impact: postgres?.currentStats.impact, - ...pick(postgres?.location, 'spanType', 'spanSubtype', 'dependencyName', 'type'), - }; - - const count = 1; - const sum = 3; - const errors = 0; - - expect(values).to.eql({ - spanType: 'external', - spanSubtype: 'http', - dependencyName: 'postgres', - type: 'dependency', - errorRate: roundNumber(errors / count), - latency: roundNumber(sum / count), - throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), - impact: 0, - }); - }); - } - ); - registry.when( 'Service overview dependencies when data is loaded', { config: 'basic', archives: [archiveName] }, @@ -311,7 +32,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { status: number; body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>; }; - // eslint-disable-next-line mocha/no-sibling-hooks + before(async () => { response = await apmApiClient.readUser({ endpoint: `GET /internal/apm/services/{serviceName}/dependencies`, diff --git a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts b/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts index ad3e872bcc879..751f772fb7507 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts @@ -6,7 +6,7 @@ */ import { take } from 'lodash'; import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; -import { ApmServices } from '../../common/config'; +import type { ApmServices } from '../../common/config'; export async function getServiceNodeIds({ apmApiClient, diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts deleted file mode 100644 index 013237934904f..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts +++ /dev/null @@ -1,156 +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 { omit } from 'lodash'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getServiceNodeIds } from './get_service_node_ids'; - -type ServiceOverviewInstanceDetails = - APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); - - const start = '2023-08-22T00:00:00.000Z'; - const end = '2023-08-22T01:00:00.000Z'; - - registry.when( - 'Instance details when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('when data is not loaded', () => { - it('handles empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', - params: { - path: { serviceName: 'opbeans-java', serviceNodeName: 'foo' }, - query: { - start, - end, - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177494 - registry.when('Instance details when data is loaded', { config: 'basic', archives: [] }, () => { - const range = timerange(new Date(start).getTime(), new Date(end).getTime()); - - const serviceInstance = apm - .service({ name: 'service1', environment: 'production', agentName: 'go' }) - .instance('multiple-env-service-production'); - - const metricOnlyInstance = apm - .service({ name: 'service1', environment: 'production', agentName: 'java' }) - .instance('multiple-env-service-production'); - - before(async () => { - return synthtrace.index([ - range - .interval('1s') - .rate(4) - .generator((timestamp) => - serviceInstance - .transaction({ transactionName: 'GET /api' }) - .timestamp(timestamp) - .duration(1000) - .success() - ), - range - .interval('30s') - .rate(1) - .generator((timestamp) => - metricOnlyInstance - .containerId('123') - .podId('234') - .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) - ), - ]); - }); - - after(() => { - return synthtrace.clean(); - }); - - describe('fetch instance details', () => { - let response: { - status: number; - body: ServiceOverviewInstanceDetails; - }; - - let serviceNodeIds: string[]; - - before(async () => { - serviceNodeIds = await getServiceNodeIds({ - apmApiClient, - start, - end, - serviceName: 'service1', - }); - - response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', - params: { - path: { serviceName: 'service1', serviceNodeName: serviceNodeIds[0] }, - query: { - start, - end, - }, - }, - }); - }); - - it('returns the instance details', () => { - expect(response.body).to.not.eql({}); - }); - - it('return the correct data', () => { - expectSnapshot(omit(response.body, '@timestamp')).toMatch(); - }); - }); - }); - - registry.when( - 'Instance details when data is loaded but details not found', - { config: 'basic', archives: [] }, - () => { - it('handles empty state when instance id not found', async () => { - const response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}', - params: { - path: { serviceName: 'opbeans-java', serviceNodeName: 'foo' }, - query: { - start, - end, - }, - }, - }); - expect(response.status).to.be(200); - expect(response.body).to.eql({}); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts index 10cd3889613ab..af28697a254c2 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.spec.ts @@ -7,11 +7,11 @@ import expect from '@kbn/expect'; import moment from 'moment'; -import { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; +import type { Coordinate } from '@kbn/apm-plugin/typings/timeseries'; import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; -import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { getServiceNodeIds } from './get_service_node_ids'; @@ -28,39 +28,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { body: APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; } - registry.when( - 'Service overview instances detailed statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', - params: { - path: { serviceName }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - start, - end, - numBuckets: 20, - transactionType: 'request', - serviceNodeIds: JSON.stringify( - await getServiceNodeIds({ apmApiClient, start, end }) - ), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); - }); - }); - } - ); - registry.when( 'Service overview instances detailed statistics when data is loaded', { config: 'basic', archives: [archiveName] }, diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts deleted file mode 100644 index b0a6bf51d1ccf..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts +++ /dev/null @@ -1,691 +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. - */ -/* - * 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 { apm, Instance, timerange } from '@kbn/apm-synthtrace-client'; -import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; -import { InstancesSortField } from '@kbn/apm-plugin/common/instances'; -import { sum } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; - -type ServiceOverviewInstancesMainStatistics = - APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); - - const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const end = new Date('2021-01-01T00:10:00.000Z').getTime(); - - async function getServiceOverviewInstancesMainStatistics({ - serviceName, - sortField = 'throughput', - sortDirection = 'desc', - }: { - serviceName: string; - sortField?: InstancesSortField; - sortDirection?: 'asc' | 'desc'; - }) { - const { body } = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, - params: { - path: { serviceName }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - transactionType: 'request', - environment: 'production', - kuery: '', - sortField, - sortDirection, - }, - }, - }); - - return body.currentPeriod; - } - - registry.when( - 'Instances main statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('when data is not loaded', () => { - it('handles empty state', async () => { - const response = await getServiceOverviewInstancesMainStatistics({ serviceName: 'foo' }); - expect(response).to.eql({}); - }); - }); - } - ); - - // FLAKY: https://github.com/elastic/kibana/issues/177492 - registry.when( - 'Instances main statistics when data is loaded', - { config: 'basic', archives: [] }, - () => { - describe('Return Top 100 instances', () => { - const serviceName = 'synth-node-1'; - before(() => { - const range = timerange(start, end); - const transactionName = 'foo'; - - const successfulTimestamps = range.interval('1m').rate(1); - const failedTimestamps = range.interval('1m').rate(1); - - const instances = [...Array(200).keys()].map((index) => - apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance(`instance-${index}`) - ); - - const instanceSpans = (instance: Instance) => { - const successfulTraceEvents = successfulTimestamps.generator((timestamp) => - instance - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .success() - .children( - instance - .span({ - spanName: 'GET apm-*/_search', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .duration(1000) - .success() - .destination('elasticsearch') - .timestamp(timestamp), - instance - .span({ spanName: 'custom_operation', spanType: 'custom' }) - .duration(100) - .success() - .timestamp(timestamp) - ) - ); - - const failedTraceEvents = failedTimestamps.generator((timestamp) => - instance - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instance - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - ); - - const metricsets = range - .interval('30s') - .rate(1) - .generator((timestamp) => - instance - .appMetrics({ - 'system.memory.actual.free': 800, - 'system.memory.total': 1000, - 'system.cpu.total.norm.pct': 0.6, - 'system.process.cpu.total.norm.pct': 0.7, - }) - .timestamp(timestamp) - ); - - return [successfulTraceEvents, failedTraceEvents, metricsets]; - }; - - return synthtrace.index(instances.flatMap((instance) => instanceSpans(instance))); - }); - - after(() => { - return synthtrace.clean(); - }); - describe('fetch instances', () => { - let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; - before(async () => { - instancesMainStats = await getServiceOverviewInstancesMainStatistics({ - serviceName, - }); - }); - it('returns top 100 instances', () => { - expect(instancesMainStats.length).to.be(100); - }); - }); - }); - - describe('Order by error rate', () => { - const serviceName = 'synth-node-1'; - before(async () => { - const range = timerange(start, end); - const transactionName = 'foo'; - /** - * Instance A - * 90 transactions = Success - * 10 transactions = Failure - * Error rate: 10% - */ - const instanceA = apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance('instance-A'); - const instanceASuccessfulTraceEvents = range - .interval('1m') - .rate(10) - .generator((timestamp, index) => - index < 10 - ? instanceA - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instanceA - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - : instanceA - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .success() - ); - /** - * Instance B - * 1 transactions = Success - * 9 transactions = Failure - * Error rate: 90% - */ - const instanceB = apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance('instance-B'); - const instanceBSuccessfulTraceEvents = range - .interval('1m') - .rate(1) - .generator((timestamp, index) => - index === 0 - ? instanceB - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .success() - : instanceB - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instanceB - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - ); - /** - * Instance C - * 2 transactions = Success - * 8 transactions = Failure - * Error rate: 80% - */ - const instanceC = apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance('instance-C'); - const instanceCSuccessfulTraceEvents = range - .interval('1m') - .rate(1) - .generator((timestamp, index) => - index < 2 - ? instanceC - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .success() - : instanceC - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instanceC - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - ); - /** - * Instance D - * 0 transactions = Success - * 10 transactions = Failure - * Error rate: 100% - */ - const instanceD = apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance('instance-D'); - const instanceDSuccessfulTraceEvents = range - .interval('1m') - .rate(1) - .generator((timestamp) => - instanceD - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .failure() - .errors( - instanceD - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - ); - /** - * Instance E - * 10 transactions = Success - * 0 transactions = Failure - * Error rate: 0% - */ - const instanceE = apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance('instance-E'); - const instanceESuccessfulTraceEvents = range - .interval('1m') - .rate(1) - .generator((timestamp) => - instanceE - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(1000) - .success() - ); - return synthtrace.index([ - instanceASuccessfulTraceEvents, - instanceBSuccessfulTraceEvents, - instanceCSuccessfulTraceEvents, - instanceDSuccessfulTraceEvents, - instanceESuccessfulTraceEvents, - ]); - }); - - after(() => { - return synthtrace.clean(); - }); - describe('sort by error rate asc', () => { - let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; - before(async () => { - instancesMainStats = await getServiceOverviewInstancesMainStatistics({ - serviceName, - sortField: 'errorRate', - sortDirection: 'asc', - }); - }); - it('returns instances sorted asc', () => { - expect(instancesMainStats.map((item) => roundNumber(item.errorRate))).to.eql([ - 0, 0.1, 0.8, 0.9, 1, - ]); - }); - }); - describe('sort by error rate desc', () => { - let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; - before(async () => { - instancesMainStats = await getServiceOverviewInstancesMainStatistics({ - serviceName, - sortField: 'errorRate', - sortDirection: 'desc', - }); - }); - it('returns instances sorted desc', () => { - expect(instancesMainStats.map((item) => roundNumber(item.errorRate))).to.eql([ - 1, 0.9, 0.8, 0.1, 0, - ]); - }); - }); - }); - - describe('with transactions and system metrics', () => { - const serviceName = 'synth-node-1'; - before(async () => { - const range = timerange(start, end); - const transactionName = 'foo'; - const instances = Array(3) - .fill(0) - .map((_, idx) => { - const index = idx + 1; - return { - instance: apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance(`instance-${index}`), - duration: index * 1000, - rate: index * 10, - errorRate: 5, - }; - }); - - return synthtrace.index( - instances.flatMap(({ instance, duration, rate, errorRate }) => { - const successfulTraceEvents = range - .interval('1m') - .rate(rate) - .generator((timestamp) => - instance - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(duration) - .success() - ); - const failedTraceEvents = range - .interval('1m') - .rate(errorRate) - .generator((timestamp) => - instance - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(duration) - .failure() - .errors( - instance - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - ); - const metricsets = range - .interval('30s') - .rate(1) - .generator((timestamp) => - instance - .appMetrics({ - 'system.memory.actual.free': 800, - 'system.memory.total': 1000, - 'system.cpu.total.norm.pct': 0.6, - 'system.process.cpu.total.norm.pct': 0.7, - }) - .timestamp(timestamp) - ); - return [successfulTraceEvents, failedTraceEvents, metricsets]; - }) - ); - }); - - after(() => { - return synthtrace.clean(); - }); - - describe('test order of items', () => { - ( - [ - { - field: 'throughput', - direction: 'asc', - expectedServiceNodeNames: ['instance-1', 'instance-2', 'instance-3'], - expectedValues: [15, 25, 35], - }, - { - field: 'throughput', - direction: 'desc', - expectedServiceNodeNames: ['instance-3', 'instance-2', 'instance-1'], - expectedValues: [35, 25, 15], - }, - { - field: 'latency', - direction: 'asc', - expectedServiceNodeNames: ['instance-1', 'instance-2', 'instance-3'], - expectedValues: [1000000, 2000000, 3000000], - }, - { - field: 'latency', - direction: 'desc', - expectedServiceNodeNames: ['instance-3', 'instance-2', 'instance-1'], - expectedValues: [3000000, 2000000, 1000000], - }, - { - field: 'serviceNodeName', - direction: 'asc', - expectedServiceNodeNames: ['instance-1', 'instance-2', 'instance-3'], - }, - { - field: 'serviceNodeName', - direction: 'desc', - expectedServiceNodeNames: ['instance-3', 'instance-2', 'instance-1'], - }, - ] as Array<{ - field: InstancesSortField; - direction: 'asc' | 'desc'; - expectedServiceNodeNames: string[]; - expectedValues?: number[]; - }> - ).map(({ field, direction, expectedServiceNodeNames, expectedValues }) => - describe(`fetch instances main statistics ordered by ${field} ${direction}`, () => { - let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; - - before(async () => { - instancesMainStats = await getServiceOverviewInstancesMainStatistics({ - serviceName, - sortField: field, - sortDirection: direction, - }); - }); - - it('returns ordered instance main stats', () => { - expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql( - expectedServiceNodeNames - ); - if (expectedValues) { - expect( - instancesMainStats.map((item) => { - const value = item[field]; - if (typeof value === 'number') { - return roundNumber(value); - } - return value; - }) - ).to.eql(expectedValues); - } - }); - - it('returns system metrics', () => { - expect(instancesMainStats.map((item) => roundNumber(item.cpuUsage))).to.eql([ - 0.7, 0.7, 0.7, - ]); - expect(instancesMainStats.map((item) => roundNumber(item.memoryUsage))).to.eql([ - 0.2, 0.2, 0.2, - ]); - }); - }) - ); - }); - }); - - describe('with transactions only', () => { - const serviceName = 'synth-node-1'; - before(async () => { - const range = timerange(start, end); - const transactionName = 'foo'; - const instances = Array(3) - .fill(0) - .map((_, idx) => { - const index = idx + 1; - return { - instance: apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance(`instance-${index}`), - duration: index * 1000, - rate: index * 10, - errorRate: 5, - }; - }); - - return synthtrace.index( - instances.flatMap(({ instance, duration, rate, errorRate }) => { - const successfulTraceEvents = range - .interval('1m') - .rate(rate) - .generator((timestamp) => - instance - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(duration) - .success() - ); - const failedTraceEvents = range - .interval('1m') - .rate(errorRate) - .generator((timestamp) => - instance - .transaction({ transactionName }) - .timestamp(timestamp) - .duration(duration) - .failure() - .errors( - instance - .error({ message: '[ResponseError] index_not_found_exception' }) - .timestamp(timestamp + 50) - ) - ); - return [successfulTraceEvents, failedTraceEvents]; - }) - ); - }); - - after(() => { - return synthtrace.clean(); - }); - - describe(`Fetch main statistics`, () => { - let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; - - before(async () => { - instancesMainStats = await getServiceOverviewInstancesMainStatistics({ - serviceName, - }); - }); - - it('returns instances name', () => { - expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql([ - 'instance-3', - 'instance-2', - 'instance-1', - ]); - }); - - it('returns throughput', () => { - expect(sum(instancesMainStats.map((item) => item.throughput))).to.greaterThan(0); - }); - - it('returns latency', () => { - expect(sum(instancesMainStats.map((item) => item.latency))).to.greaterThan(0); - }); - - it('returns errorRate', () => { - expect(sum(instancesMainStats.map((item) => item.errorRate))).to.greaterThan(0); - }); - - it('does not return cpu usage', () => { - expect( - instancesMainStats.map((item) => item.cpuUsage).filter((value) => value !== undefined) - ).to.eql([]); - }); - - it('does not return memory usage', () => { - expect( - instancesMainStats - .map((item) => item.memoryUsage) - .filter((value) => value !== undefined) - ).to.eql([]); - }); - }); - }); - - describe('with system metrics only', () => { - const serviceName = 'synth-node-1'; - before(async () => { - const range = timerange(start, end); - const instances = Array(3) - .fill(0) - .map((_, idx) => - apm - .service({ name: serviceName, environment: 'production', agentName: 'nodejs' }) - .instance(`instance-${idx + 1}`) - ); - - return synthtrace.index( - instances.map((instance) => { - const metricsets = range - .interval('30s') - .rate(1) - .generator((timestamp) => - instance - .appMetrics({ - 'system.memory.actual.free': 800, - 'system.memory.total': 1000, - 'system.cpu.total.norm.pct': 0.6, - 'system.process.cpu.total.norm.pct': 0.7, - }) - .timestamp(timestamp) - ); - return metricsets; - }) - ); - }); - - after(() => { - return synthtrace.clean(); - }); - - describe(`Fetch main statistics`, () => { - let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; - - before(async () => { - instancesMainStats = await getServiceOverviewInstancesMainStatistics({ - serviceName, - }); - }); - - it('returns instances name', () => { - expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql([ - 'instance-1', - 'instance-2', - 'instance-3', - ]); - }); - - it('does not return throughput', () => { - expect( - instancesMainStats - .map((item) => item.throughput) - .filter((value) => value !== undefined) - ).to.eql([]); - }); - - it('does not return latency', () => { - expect( - instancesMainStats.map((item) => item.latency).filter((value) => value !== undefined) - ).to.eql([]); - }); - - it('does not return errorRate', () => { - expect( - instancesMainStats - .map((item) => item.errorRate) - .filter((value) => value !== undefined) - ).to.eql([]); - }); - - it('returns cpu usage', () => { - expect(sum(instancesMainStats.map((item) => item.cpuUsage))).to.greaterThan(0); - }); - - it('returns memory usage', () => { - expect(sum(instancesMainStats.map((item) => item.memoryUsage))).to.greaterThan(0); - }); - }); - }); - } - ); -}