diff --git a/x-pack/plugins/apm/common/instances.ts b/x-pack/plugins/apm/common/instances.ts new file mode 100644 index 000000000000000..8a1cdb4eed538fb --- /dev/null +++ b/x-pack/plugins/apm/common/instances.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; + +export const instancesSortFieldRt = t.keyof({ + serviceNodeName: null, + latency: null, + throughput: null, + errorRate: null, + cpuUsage: null, + memoryUsage: null, +}); + +export type InstancesSortField = t.TypeOf; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index ab6d66ab2130dd3..f9c084f89018f7c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -6,7 +6,6 @@ */ import { EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { orderBy } from 'lodash'; import React, { useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options'; @@ -25,6 +24,7 @@ import { TableOptions, } from './service_overview_instances_table'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { InstancesSortField } from '../../../../common/instances'; interface ServiceOverviewInstancesChartAndTableProps { chartHeight: number; @@ -48,14 +48,6 @@ const INITIAL_STATE_DETAILED_STATISTICS: ApiResponseDetailedStats = { previousPeriod: {}, }; -export type SortField = - | 'serviceNodeName' - | 'latency' - | 'throughput' - | 'errorRate' - | 'cpuUsage' - | 'memoryUsage'; - export type SortDirection = 'asc' | 'desc'; export const PAGE_SIZE = 5; const DEFAULT_SORT = { @@ -122,6 +114,8 @@ export function ServiceOverviewInstancesChartAndTable({ comparisonEnabled && isTimeComparison(offset) ? offset : undefined, + sortField: tableOptions.sort.field, + sortDirection: tableOptions.sort.direction, }, }, } @@ -152,6 +146,7 @@ export function ServiceOverviewInstancesChartAndTable({ offset, // not used, but needed to trigger an update when comparison feature is disabled/enabled by user comparisonEnabled, + tableOptions.sort, ] ); @@ -162,19 +157,10 @@ export function ServiceOverviewInstancesChartAndTable({ currentPeriodItemsCount, } = mainStatsData; - const currentPeriodOrderedItems = orderBy( - // need top-level sortable fields for the managed table - currentPeriodItems.map((item) => ({ - ...item, - latency: item.latency ?? 0, - throughput: item.throughput ?? 0, - errorRate: item.errorRate ?? 0, - cpuUsage: item.cpuUsage ?? 0, - memoryUsage: item.memoryUsage ?? 0, - })), - field, - direction - ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + const currentPageItems = currentPeriodItems.slice( + pageIndex * PAGE_SIZE, + (pageIndex + 1) * PAGE_SIZE + ); const { data: detailedStatsData = INITIAL_STATE_DETAILED_STATISTICS, @@ -208,7 +194,7 @@ export function ServiceOverviewInstancesChartAndTable({ numBuckets: 20, transactionType, serviceNodeIds: JSON.stringify( - currentPeriodOrderedItems.map((item) => item.serviceNodeName) + currentPageItems.map((item) => item.serviceNodeName) ), offset: comparisonEnabled && isTimeComparison(offset) @@ -238,7 +224,7 @@ export function ServiceOverviewInstancesChartAndTable({ { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.cpuUsage; @@ -241,7 +240,6 @@ export function getColumns({ /> ); }, - sortable: true, }, { field: 'memoryUsage', @@ -250,6 +248,7 @@ export function getColumns({ { defaultMessage: 'Memory usage (avg.)' } ), align: RIGHT_ALIGNMENT, + sortable: true, render: (_, { serviceNodeName, memoryUsage }) => { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.memoryUsage; @@ -277,7 +276,6 @@ export function getColumns({ /> ); }, - sortable: true, }, { width: '40px', @@ -292,7 +290,10 @@ export function getColumns({ anchorPosition="leftCenter" button={ diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index 5fad00dfa34ff68..5e1a466864960e0 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -13,13 +13,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ReactNode, useEffect, useState } from 'react'; -import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; import { PAGE_SIZE, SortDirection, - SortField, } from '../service_overview_instances_chart_and_table'; import { OverviewTableContainer } from '../../../shared/overview_table_container'; import { getColumns } from './get_columns'; @@ -27,6 +25,7 @@ import { InstanceDetails } from './intance_details'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useBreakpoints } from '../../../../hooks/use_breakpoints'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; +import { InstancesSortField } from '../../../../../common/instances'; type ServiceInstanceMainStatistics = APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics'>; @@ -39,7 +38,7 @@ export interface TableOptions { pageIndex: number; sort: { direction: SortDirection; - field: SortField; + field: InstancesSortField; }; } @@ -70,8 +69,6 @@ export function ServiceOverviewInstancesTable({ isLoading, isNotInitiated, }: Props) { - const { agentName } = useApmServiceContext(); - const { query: { kuery, latencyAggregationType, comparisonEnabled, offset }, } = useApmParams('/services/{serviceName}'); @@ -122,7 +119,6 @@ export function ServiceOverviewInstancesTable({ const shouldShowSparkPlots = !isXl; const columns = getColumns({ - agentName, serviceName, kuery, latencyAggregationType: latencyAggregationType as LatencyAggregationType, @@ -154,7 +150,9 @@ export function ServiceOverviewInstancesTable({

{i18n.translate('xpack.apm.serviceOverview.instancesTableTitle', { - defaultMessage: 'Instances', + defaultMessage: + 'Top {count} {count, plural, one {instance} other {instances}}', + values: { count: mainStatsItemCount }, })}

diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instances/detailed_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_instances/detailed_statistics.ts index 28401dd0b46b5c4..95f56520b8ab2b4 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instances/detailed_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instances/detailed_statistics.ts @@ -51,11 +51,11 @@ async function getServiceInstancesDetailedStatistics( const [transactionStats, systemMetricStats = []] = await Promise.all([ getServiceInstancesTransactionStatistics({ ...params, - isComparisonSearch: true, + includeTimeseries: true, }), getServiceInstancesSystemMetricStatistics({ ...params, - isComparisonSearch: true, + includeTimeseries: true, }), ]); diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_system_metric_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_system_metric_statistics.ts index b2925af05946acf..775246e5a578d2a 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_system_metric_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_system_metric_statistics.ts @@ -52,7 +52,7 @@ export async function getServiceInstancesSystemMetricStatistics< end, serviceNodeIds, numBuckets, - isComparisonSearch, + includeTimeseries, offset, }: { apmEventClient: APMEventClient; @@ -64,7 +64,7 @@ export async function getServiceInstancesSystemMetricStatistics< environment: string; kuery: string; size?: number; - isComparisonSearch: T; + includeTimeseries: T; offset?: string; }): Promise>> { const { startWithOffset, endWithOffset } = getOffsetInMs({ @@ -85,7 +85,7 @@ export async function getServiceInstancesSystemMetricStatistics< agg: TParams ) { return { - ...(isComparisonSearch + ...(includeTimeseries ? { avg: { avg: agg }, timeseries: { @@ -136,7 +136,7 @@ export async function getServiceInstancesSystemMetricStatistics< ...rangeQuery(startWithOffset, endWithOffset), ...environmentQuery(environment), ...kqlQuery(kuery), - ...(isComparisonSearch && serviceNodeIds + ...(serviceNodeIds?.length ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }] : []), { @@ -158,7 +158,7 @@ export async function getServiceInstancesSystemMetricStatistics< field: SERVICE_NODE_NAME, missing: SERVICE_NODE_NAME_MISSING, ...(size ? { size } : {}), - ...(isComparisonSearch ? { include: serviceNodeIds } : {}), + ...(serviceNodeIds?.length ? { include: serviceNodeIds } : {}), }, aggs: subAggs, }, @@ -179,7 +179,7 @@ export async function getServiceInstancesSystemMetricStatistics< : 'memory_usage_system'; const cpuUsage = - // Timeseries is available when isComparisonSearch is true + // Timeseries is available when includeTimeseries is true 'timeseries' in serviceNodeBucket.cpu_usage ? serviceNodeBucket.cpu_usage.timeseries.buckets.map( (dateBucket) => ({ @@ -191,7 +191,7 @@ export async function getServiceInstancesSystemMetricStatistics< const memoryUsageValue = serviceNodeBucket[memoryMetricsKey]; const memoryUsage = - // Timeseries is available when isComparisonSearch is true + // Timeseries is available when includeTimeseries is true 'timeseries' in memoryUsageValue ? memoryUsageValue.timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts index ab687382e767bfb..db9392fba1b4b5c 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts @@ -63,7 +63,7 @@ export async function getServiceInstancesTransactionStatistics< end, serviceNodeIds, numBuckets, - isComparisonSearch, + includeTimeseries, offset, }: { latencyAggregationType: LatencyAggregationType; @@ -73,7 +73,7 @@ export async function getServiceInstancesTransactionStatistics< searchAggregatedTransactions: boolean; start: number; end: number; - isComparisonSearch: T; + includeTimeseries: T; serviceNodeIds?: string[]; environment: string; kuery: string; @@ -123,7 +123,7 @@ export async function getServiceInstancesTransactionStatistics< ...getBackwardCompatibleDocumentTypeFilter( searchAggregatedTransactions ), - ...(isComparisonSearch && serviceNodeIds + ...(serviceNodeIds?.length ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }] : []), ], @@ -136,9 +136,9 @@ export async function getServiceInstancesTransactionStatistics< field: SERVICE_NODE_NAME, missing: SERVICE_NODE_NAME_MISSING, ...(size ? { size } : {}), - ...(isComparisonSearch ? { include: serviceNodeIds } : {}), + ...(serviceNodeIds?.length ? { include: serviceNodeIds } : {}), }, - aggs: isComparisonSearch + aggs: includeTimeseries ? { timeseries: { date_histogram: { @@ -174,7 +174,7 @@ export async function getServiceInstancesTransactionStatistics< const { doc_count: count, key } = serviceNodeBucket; const serviceNodeName = String(key); - // Timeseries is returned when isComparisonSearch is true + // Timeseries is returned when includeTimeseries is true if ('timeseries' in serviceNodeBucket) { const { timeseries } = serviceNodeBucket; return { diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instances/main_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_instances/main_statistics.ts index 16a9499979561d3..bf567aa98c84c22 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instances/main_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instances/main_statistics.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { keyBy, orderBy } from 'lodash'; +import { InstancesSortField } from '../../../../common/instances'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { joinByKey } from '../../../../common/utils/join_by_key'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { withApmSpan } from '../../../utils/with_apm_span'; import { getServiceInstancesSystemMetricStatistics } from './get_service_instances_system_metric_statistics'; import { getServiceInstancesTransactionStatistics } from './get_service_instances_transaction_statistics'; @@ -24,6 +25,8 @@ interface ServiceInstanceMainStatisticsParams { start: number; end: number; offset?: string; + sortField: InstancesSortField; + sortDirection: 'asc' | 'desc'; } export type ServiceInstanceMainStatisticsResponse = Array<{ @@ -35,31 +38,39 @@ export type ServiceInstanceMainStatisticsResponse = Array<{ memoryUsage?: number | null; }>; -export async function getServiceInstancesMainStatistics( - params: Omit -): Promise { +export async function getServiceInstancesMainStatistics({ + sortDirection, + sortField, + ...params +}: Omit< + ServiceInstanceMainStatisticsParams, + 'size' +>): Promise { return withApmSpan('get_service_instances_main_statistics', async () => { const paramsForSubQueries = { ...params, - size: 50, + size: 1000, }; - const [transactionStats, systemMetricStats] = await Promise.all([ - getServiceInstancesTransactionStatistics({ - ...paramsForSubQueries, - isComparisonSearch: false, - }), - getServiceInstancesSystemMetricStatistics({ - ...paramsForSubQueries, - isComparisonSearch: false, - }), - ]); + const transactionStats = await getServiceInstancesTransactionStatistics({ + ...paramsForSubQueries, + includeTimeseries: false, + }); + const serviceNodeIds = transactionStats.map((item) => item.serviceNodeName); + const systemMetricStats = await getServiceInstancesSystemMetricStatistics({ + ...paramsForSubQueries, + includeTimeseries: false, + serviceNodeIds, + }); - const stats = joinByKey( - [...transactionStats, ...systemMetricStats], - 'serviceNodeName' - ); + const systemMetricStatsMap = keyBy(systemMetricStats, 'serviceNodeName'); + const stats = transactionStats.length + ? transactionStats.map((item) => ({ + ...item, + ...(systemMetricStatsMap[item.serviceNodeName] || {}), + })) + : systemMetricStats; - return stats; + return orderBy(stats, sortField, sortDirection).slice(0, 100); }); } diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index e997c6d56c1b807..550cab6e76249c7 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -24,6 +24,7 @@ import { mergeWith, uniq } from 'lodash'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { ServiceAnomalyTimeseries } from '../../../common/anomaly_detection/service_anomaly_timeseries'; import { offsetRt } from '../../../common/comparison_rt'; +import { instancesSortFieldRt } from '../../../common/instances'; import { latencyAggregationTypeRt } from '../../../common/latency_aggregation_types'; import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate'; import { getAnomalyTimeseries } from '../../lib/anomaly_detection/get_anomaly_timeseries'; @@ -618,6 +619,8 @@ const serviceInstancesMainStatisticsRoute = createApmServerRoute({ t.type({ latencyAggregationType: latencyAggregationTypeRt, transactionType: t.string, + sortField: instancesSortFieldRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), }), offsetRt, environmentRt, @@ -643,6 +646,8 @@ const serviceInstancesMainStatisticsRoute = createApmServerRoute({ offset, start, end, + sortField, + sortDirection, } = params.query; const searchAggregatedTransactions = await getSearchTransactionsEvents({ @@ -653,33 +658,24 @@ const serviceInstancesMainStatisticsRoute = createApmServerRoute({ end, }); + const commonParams = { + environment, + kuery, + latencyAggregationType, + serviceName, + apmEventClient, + transactionType, + searchAggregatedTransactions, + start, + end, + sortField, + sortDirection, + }; + const [currentPeriod, previousPeriod] = await Promise.all([ - getServiceInstancesMainStatistics({ - environment, - kuery, - latencyAggregationType, - serviceName, - apmEventClient, - transactionType, - searchAggregatedTransactions, - start, - end, - }), + getServiceInstancesMainStatistics(commonParams), ...(offset - ? [ - getServiceInstancesMainStatistics({ - environment, - kuery, - latencyAggregationType, - serviceName, - apmEventClient, - transactionType, - searchAggregatedTransactions, - start, - end, - offset, - }), - ] + ? [getServiceInstancesMainStatistics({ ...commonParams, offset })] : []), ]); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e4dab6524c3f4b0..0155d9d1de5f272 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9147,7 +9147,6 @@ "xpack.apm.serviceOverview.instancesTableColumnMemoryUsage": "Utilisation de la mémoire (moy.)", "xpack.apm.serviceOverview.instancesTableColumnNodeName": "Nom du nœud", "xpack.apm.serviceOverview.instancesTableColumnThroughput": "Rendement", - "xpack.apm.serviceOverview.instancesTableTitle": "Instances", "xpack.apm.serviceOverview.instanceTable.details.cloudTitle": "Cloud", "xpack.apm.serviceOverview.instanceTable.details.containerTitle": "Conteneur", "xpack.apm.serviceOverview.instanceTable.details.serviceTitle": "Service", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0bfb0520186b3e5..6ec1330d0b14ea1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9161,7 +9161,6 @@ "xpack.apm.serviceOverview.instancesTableColumnMemoryUsage": "メモリー使用状況(平均)", "xpack.apm.serviceOverview.instancesTableColumnNodeName": "ノード名", "xpack.apm.serviceOverview.instancesTableColumnThroughput": "スループット", - "xpack.apm.serviceOverview.instancesTableTitle": "インスタンス", "xpack.apm.serviceOverview.instanceTable.details.cloudTitle": "クラウド", "xpack.apm.serviceOverview.instanceTable.details.containerTitle": "コンテナー", "xpack.apm.serviceOverview.instanceTable.details.serviceTitle": "サービス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7e238a0c10c6443..ba48546d7999e9a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9255,7 +9255,6 @@ "xpack.apm.serviceOverview.instancesTableColumnMemoryUsage": "内存使用率(平均值)", "xpack.apm.serviceOverview.instancesTableColumnNodeName": "节点名称", "xpack.apm.serviceOverview.instancesTableColumnThroughput": "吞吐量", - "xpack.apm.serviceOverview.instancesTableTitle": "实例", "xpack.apm.serviceOverview.instanceTable.details.cloudTitle": "云", "xpack.apm.serviceOverview.instanceTable.details.containerTitle": "容器", "xpack.apm.serviceOverview.instanceTable.details.serviceTitle": "服务", diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts index bdcd2f30c1f8430..09ad6c631cbfa5e 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts @@ -113,6 +113,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { kuery: `processor.event : "${processorEvent}"`, transactionType: 'request', latencyAggregationType: 'avg' as LatencyAggregationType, + sortField: 'throughput', + sortDirection: 'desc', }, }, }), diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts index d174bcdf03411c8..a3904e77e26ec5b 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts @@ -118,6 +118,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { kuery: `processor.event : "${processorEvent}"`, transactionType: 'request', latencyAggregationType: 'avg' as LatencyAggregationType, + sortField: 'throughput', + sortDirection: 'desc', }, }, }), 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 index 022048cdbd5ed59..f3fb16ec38b1568 100644 --- 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 @@ -9,7 +9,7 @@ Object { "id": "123", }, "host": Object { - "name": "metric-only-production", + "name": "multiple-env-service-production", }, "kubernetes": Object { "container": Object {}, @@ -23,7 +23,7 @@ Object { "environment": "production", "name": "service1", "node": Object { - "name": "metric-only-production", + "name": "multiple-env-service-production", }, }, } 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 8736b46f734599d..ad3e872bcc879c6 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 @@ -32,6 +32,8 @@ export async function getServiceNodeIds({ transactionType: 'request', environment: 'ENVIRONMENT_ALL', kuery: '', + sortField: 'throughput', + sortDirection: 'desc', }, }, }); 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 index f8895b73ab0dbdb..4ff96129dfcf92f 100644 --- 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 @@ -56,7 +56,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const metricOnlyInstance = apm .service({ name: 'service1', environment: 'production', agentName: 'java' }) - .instance('metric-only-production'); + .instance('multiple-env-service-production'); before(async () => { return synthtrace.index([ 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 index f215882c09bd21d..4e92e657de49f66 100644 --- 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 @@ -4,436 +4,684 @@ * 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 { pick, sortBy } from 'lodash'; -import moment from 'moment'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number'; - +import { apm, Instance, timerange } from '@kbn/apm-synthtrace-client'; import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; -import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values'; -import { SERVICE_NODE_NAME_MISSING } from '@kbn/apm-plugin/common/service_nodes'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; +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 synthtraceEsClient = getService('synthtraceEsClient'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; + const synthtrace = getService('synthtraceEsClient'); + + 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( - 'Service overview instances main statistics when data is not loaded', + 'Instances main 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/main_statistics`, - params: { - path: { serviceName: 'opbeans-java' }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - transactionType: 'request', - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - offset: '15m', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.eql([]); - expect(response.body.previousPeriod).to.eql([]); + it('handles empty state', async () => { + const response = await getServiceOverviewInstancesMainStatistics({ serviceName: 'foo' }); + expect(response).to.eql({}); }); }); } ); registry.when( - 'Service overview instances main statistics when data is loaded without comparison', - { config: 'basic', archives: [archiveName] }, + 'Instances main statistics when data is loaded', + { config: 'basic', archives: [] }, () => { - describe('fetching java data', () => { - let response: { - body: APIReturnType<`GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`>; - }; - - beforeEach(async () => { - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, - params: { - path: { serviceName: 'opbeans-java' }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - start, - end, - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }); - }); - - it('returns a service node item', () => { - expect(response.body.currentPeriod.length).to.be.greaterThan(0); - }); + 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) + ) + ); - it('returns statistics for each service node', () => { - const item = response.body.currentPeriod[0]; + 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) + ) + ); - expect(isFiniteNumber(item.cpuUsage)).to.be(true); - expect(isFiniteNumber(item.memoryUsage)).to.be(true); - expect(isFiniteNumber(item.errorRate)).to.be(true); - expect(isFiniteNumber(item.throughput)).to.be(true); - expect(isFiniteNumber(item.latency)).to.be(true); + 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))); }); - it('returns the right data', () => { - const items = sortBy(response.body.currentPeriod, 'serviceNodeName'); - - const serviceNodeNames = items.map((item) => item.serviceNodeName); - - expectSnapshot(items.length).toMatchInline(`1`); - - expectSnapshot(serviceNodeNames).toMatchInline(` - Array [ - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", - ] - `); - - const item = items[0]; + 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); + }); + }); + }); - const values = pick(item, [ - 'cpuUsage', - 'memoryUsage', - 'errorRate', - 'throughput', - 'latency', + 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, ]); - - expectSnapshot(values).toMatchInline(` - Object { - "cpuUsage": 0.002, - "errorRate": 0.0848214285714286, - "latency": 411589.785714286, - "memoryUsage": 0.786029688517253, - "throughput": 7.46666666666667, - } - `); }); - }); - describe('fetching non-java data', () => { - let response: { - body: APIReturnType<`GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`>; - }; - - beforeEach(async () => { - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, - params: { - path: { serviceName: 'opbeans-ruby' }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - start, - end, - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, + 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, + ]); }); }); - - it('returns statistics for each service node', () => { - const item = response.body.currentPeriod[0]; - - expect(isFiniteNumber(item.cpuUsage)).to.be(true); - expect(isFiniteNumber(item.memoryUsage)).to.be(true); - expect(isFiniteNumber(item.errorRate)).to.be(true); - expect(isFiniteNumber(item.throughput)).to.be(true); - expect(isFiniteNumber(item.latency)).to.be(true); + 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, + ]); + }); }); + }); - it('returns the right data', () => { - const items = sortBy(response.body.currentPeriod, 'serviceNodeName'); - - const serviceNodeNames = items.map((item) => item.serviceNodeName); - - expectSnapshot(items.length).toMatchInline(`1`); - - expectSnapshot(serviceNodeNames).toMatchInline(` - Array [ - "b4c600993a0b233120cd333b8c4a7e35e73ee8f18f95b5854b8d7f6442531466", - ] - `); - - const item = items[0]; - - const values = pick(item, 'cpuUsage', 'errorRate', 'throughput', 'latency'); - - expectSnapshot(values).toMatchInline(` - Object { - "cpuUsage": 0.001, - "errorRate": 0.00341296928327645, - "latency": 40989.5802047782, - "throughput": 9.76666666666667, - } - `); + 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]; + }) + ); + }); - expectSnapshot(values); + after(() => { + return synthtrace.clean(); }); - }); - } - ); - registry.when( - 'Service overview instances main statistics when data is loaded with comparison', - { config: 'basic', archives: [archiveName] }, - () => { - describe('fetching java data', () => { - let response: { - body: APIReturnType<`GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`>; - }; - - beforeEach(async () => { - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, - params: { - path: { serviceName: 'opbeans-java' }, - query: { - latencyAggregationType: LatencyAggregationType.avg, - transactionType: 'request', - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - offset: '15m', - environment: 'ENVIRONMENT_ALL', - kuery: '', + 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, + ]); + }); + }) + ); }); + }); - it('returns a service node item', () => { - expect(response.body.currentPeriod.length).to.be.greaterThan(0); - expect(response.body.previousPeriod.length).to.be.greaterThan(0); + 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]; + }) + ); }); - it('returns statistics for each service node', () => { - const currentItem = response.body.currentPeriod[0]; - - expect(isFiniteNumber(currentItem.cpuUsage)).to.be(true); - expect(isFiniteNumber(currentItem.memoryUsage)).to.be(true); - expect(isFiniteNumber(currentItem.errorRate)).to.be(true); - expect(isFiniteNumber(currentItem.throughput)).to.be(true); - expect(isFiniteNumber(currentItem.latency)).to.be(true); - - const previousItem = response.body.previousPeriod[0]; - - expect(isFiniteNumber(previousItem.cpuUsage)).to.be(true); - expect(isFiniteNumber(previousItem.memoryUsage)).to.be(true); - expect(isFiniteNumber(previousItem.errorRate)).to.be(true); - expect(isFiniteNumber(previousItem.throughput)).to.be(true); - expect(isFiniteNumber(previousItem.latency)).to.be(true); + after(() => { + return synthtrace.clean(); }); - it('returns the right data', () => { - const items = sortBy(response.body.previousPeriod, 'serviceNodeName'); + describe(`Fetch main statistics`, () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; - const serviceNodeNames = items.map((item) => item.serviceNodeName); + before(async () => { + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + }); + }); - expectSnapshot(items.length).toMatchInline(`1`); + it('returns instances name', () => { + expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql([ + 'instance-3', + 'instance-2', + 'instance-1', + ]); + }); - expectSnapshot(serviceNodeNames).toMatchInline(` - Array [ - "31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad", - ] - `); + it('returns throughput', () => { + expect(sum(instancesMainStats.map((item) => item.throughput))).to.greaterThan(0); + }); - const item = items[0]; + it('returns latency', () => { + expect(sum(instancesMainStats.map((item) => item.latency))).to.greaterThan(0); + }); - const values = pick(item, [ - 'cpuUsage', - 'memoryUsage', - 'errorRate', - 'throughput', - 'latency', - ]); + 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([]); + }); - expectSnapshot(values).toMatchInline(` - Object { - "cpuUsage": 0.00223333333333333, - "errorRate": 0.0894308943089431, - "latency": 739013.634146341, - "memoryUsage": 0.783296203613281, - "throughput": 8.2, - } - `); + it('does not return memory usage', () => { + expect( + instancesMainStats + .map((item) => item.memoryUsage) + .filter((value) => value !== undefined) + ).to.eql([]); + }); }); }); - } - ); - - registry.when( - 'Service overview instances main statistics when data is generated', - { config: 'basic', archives: [] }, - () => { - describe('for two go instances and one java instance', () => { - const GO_A_INSTANCE_RATE_SUCCESS = 10; - const GO_A_INSTANCE_RATE_FAILURE = 5; - const GO_B_INSTANCE_RATE_SUCCESS = 15; - - const JAVA_INSTANCE_RATE = 20; - - const rangeStart = new Date('2021-01-01T12:00:00.000Z').getTime(); - const rangeEnd = new Date('2021-01-01T12:15:00.000Z').getTime() - 1; + describe('with system metrics only', () => { + const serviceName = 'synth-node-1'; before(async () => { - const goService = apm.service({ - name: 'opbeans-go', - environment: 'production', - agentName: 'go', - }); - const javaService = apm.service({ - name: 'opbeans-java', - environment: 'production', - agentName: 'java', - }); - - const goInstanceA = goService.instance('go-instance-a'); - const goInstanceB = goService.instance('go-instance-b'); - const javaInstance = javaService.instance('java-instance'); - - const interval = timerange(rangeStart, rangeEnd).interval('1m'); - - // include exit spans to generate span_destination metrics - // that should not be included - function withSpans(timestamp: number) { - return new Array(3).fill(undefined).map(() => - goInstanceA - .span({ - spanName: 'GET apm-*/_search', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .timestamp(timestamp + 100) - .duration(300) - .destination('elasticsearch') - .success() + 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 synthtraceEsClient.index([ - interval.rate(GO_A_INSTANCE_RATE_SUCCESS).generator((timestamp) => - goInstanceA - .transaction({ transactionName: 'GET /api/product/list' }) - .success() - .duration(500) - .timestamp(timestamp) - .children(...withSpans(timestamp)) - ), - interval.rate(GO_A_INSTANCE_RATE_FAILURE).generator((timestamp) => - goInstanceA - .transaction({ transactionName: 'GET /api/product/list' }) - .failure() - .duration(500) - .timestamp(timestamp) - .children(...withSpans(timestamp)) - ), - interval.rate(GO_B_INSTANCE_RATE_SUCCESS).generator((timestamp) => - goInstanceB - .transaction({ transactionName: 'GET /api/product/list' }) - .success() - .duration(500) - .timestamp(timestamp) - .children(...withSpans(timestamp)) - ), - interval.rate(JAVA_INSTANCE_RATE).generator((timestamp) => - javaInstance - .transaction({ transactionName: 'GET /api/product/list' }) - .success() - .duration(500) - .timestamp(timestamp) - .children(...withSpans(timestamp)) - ), - ]); + 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(async () => { - return synthtraceEsClient.clean(); + after(() => { + return synthtrace.clean(); }); - describe('for the go service', () => { - let body: APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics'>; + describe(`Fetch main statistics`, () => { + let instancesMainStats: ServiceOverviewInstancesMainStatistics['currentPeriod']; before(async () => { - body = ( - await apmApiClient.readUser({ - endpoint: - 'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics', - params: { - path: { - serviceName: 'opbeans-go', - }, - query: { - start: new Date(rangeStart).toISOString(), - end: new Date(rangeEnd + 1).toISOString(), - environment: ENVIRONMENT_ALL.value, - kuery: '', - latencyAggregationType: LatencyAggregationType.avg, - transactionType: 'request', - }, - }, - }) - ).body; + instancesMainStats = await getServiceOverviewInstancesMainStatistics({ + serviceName, + }); }); - it('returns statistics for the go instances', () => { - const goAStats = body.currentPeriod.find( - (stat) => stat.serviceNodeName === 'go-instance-a' - ); - const goBStats = body.currentPeriod.find( - (stat) => stat.serviceNodeName === 'go-instance-b' - ); - - expect(goAStats?.throughput).to.eql( - GO_A_INSTANCE_RATE_SUCCESS + GO_A_INSTANCE_RATE_FAILURE - ); - - expect(goBStats?.throughput).to.eql(GO_B_INSTANCE_RATE_SUCCESS); + it('returns instances name', () => { + expect(instancesMainStats.map((item) => item.serviceNodeName)).to.eql([ + 'instance-1', + 'instance-2', + 'instance-3', + ]); }); - it('does not return data for the java service', () => { - const javaStats = body.currentPeriod.find( - (stat) => stat.serviceNodeName === 'java-instance' - ); - - expect(javaStats).to.be(undefined); + it('does not return throughput', () => { + expect( + instancesMainStats + .map((item) => item.throughput) + .filter((value) => value !== undefined) + ).to.eql([]); }); - it('does not return metrics', () => { - const goAStats = body.currentPeriod.find( - (stat) => stat.serviceNodeName === 'go-instance-a' - ); + it('does not return latency', () => { + expect( + instancesMainStats.map((item) => item.latency).filter((value) => value !== undefined) + ).to.eql([]); + }); - expect(goAStats).to.not.be(undefined); - expect(goAStats?.memoryUsage).to.be(undefined); - expect(goAStats?.cpuUsage).to.be(undefined); + it('does not return errorRate', () => { + expect( + instancesMainStats + .map((item) => item.errorRate) + .filter((value) => value !== undefined) + ).to.eql([]); }); - it('does not return data for missing service node name', () => { - const missingNameStats = body.currentPeriod.find( - (stat) => stat.serviceNodeName === SERVICE_NODE_NAME_MISSING - ); + it('returns cpu usage', () => { + expect(sum(instancesMainStats.map((item) => item.cpuUsage))).to.greaterThan(0); + }); - expect(missingNameStats).to.be(undefined); + it('returns memory usage', () => { + expect(sum(instancesMainStats.map((item) => item.memoryUsage))).to.greaterThan(0); }); }); }); diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts index 8294797a0894989..8dcb782c78a03f3 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts @@ -109,6 +109,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { kuery: `processor.event : "${processorEvent}"`, transactionType: 'request', latencyAggregationType: 'avg' as LatencyAggregationType, + sortField: 'throughput', + sortDirection: 'desc', }, }, }),