From 2fcf2a91cc870c4dfe52d1107885cefb40e0100e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 12 Feb 2021 11:57:01 +0100 Subject: [PATCH] [APM] Break down transaction table api removing the sparklines (#88946) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dario Gieselaar --- .../service_overview.test.tsx | 6 +- .../get_columns.tsx | 163 ++++++ .../index.tsx | 295 ++++------ .../shared/charts/spark_plot/index.tsx | 17 +- .../url_params_context/resolve_url_params.ts | 2 +- .../context/url_params_context/types.ts | 3 +- .../use_transaction_latency_chart_fetcher.ts | 3 +- ...transaction_group_comparison_statistics.ts | 167 ++++++ ...e.ts => get_service_transaction_groups.ts} | 109 ++-- ..._timeseries_data_for_transaction_groups.ts | 119 ---- .../get_service_transaction_groups/index.ts | 89 --- .../merge_transaction_group_data.ts | 90 --- .../apm/server/routes/create_apm_api.ts | 6 +- .../plugins/apm/server/routes/transactions.ts | 97 ++-- .../test/apm_api_integration/tests/index.ts | 3 +- .../service_overview/dependencies/index.ts | 43 +- ...sactions_groups_comparison_statistics.snap | 517 ++++++++++++++++++ ...ansactions_groups_comparison_statistics.ts | 158 ++++++ .../transactions_groups_overview.ts | 273 --------- .../transactions_groups_primary_statistics.ts | 150 +++++ x-pack/test/apm_api_integration/utils.ts | 18 + .../typings/elasticsearch/aggregations.d.ts | 12 + 22 files changed, 1426 insertions(+), 914 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts rename x-pack/plugins/apm/server/lib/services/{get_service_transaction_groups/get_transaction_groups_for_page.ts => get_service_transaction_groups.ts} (50%) delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts create mode 100644 x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap create mode 100644 x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts delete mode 100644 x-pack/test/apm_api_integration/tests/transactions/transactions_groups_overview.ts create mode 100644 x-pack/test/apm_api_integration/tests/transactions/transactions_groups_primary_statistics.ts create mode 100644 x-pack/test/apm_api_integration/utils.ts diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index cb77c0fa1346..999718e754c6 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -80,21 +80,21 @@ describe('ServiceOverview', () => { status: FETCH_STATUS.SUCCESS, }); + /* eslint-disable @typescript-eslint/naming-convention */ const calls = { - // eslint-disable-next-line @typescript-eslint/naming-convention 'GET /api/apm/services/{serviceName}/error_groups': { error_groups: [], total_error_groups: 0, }, - 'GET /api/apm/services/{serviceName}/transactions/groups/overview': { + 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': { transactionGroups: [], totalTransactionGroups: 0, isAggregationAccurate: true, }, 'GET /api/apm/services/{serviceName}/dependencies': [], - // eslint-disable-next-line @typescript-eslint/naming-convention 'GET /api/apm/services/{serviceName}/service_overview_instances': [], }; + /* eslint-enable @typescript-eslint/naming-convention */ jest .spyOn(callApmApiModule, 'createCallApmApi') diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx new file mode 100644 index 000000000000..2ffc0fc9c93a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx @@ -0,0 +1,163 @@ +/* + * 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 { EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ValuesType } from 'utility-types'; +import { + asMillisecondDuration, + asPercent, + asTransactionRate, +} from '../../../../../common/utils/formatters'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +import { px, unit } from '../../../../style/variables'; +import { SparkPlot } from '../../../shared/charts/spark_plot'; +import { ImpactBar } from '../../../shared/ImpactBar'; +import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; + +type TransactionGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics'>; + +type ServiceTransactionGroupItem = ValuesType< + TransactionGroupPrimaryStatistics['transactionGroups'] +>; +type TransactionGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics'>; + +function getLatencyAggregationTypeLabel(latencyAggregationType?: string) { + switch (latencyAggregationType) { + case 'avg': + return i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', + { defaultMessage: 'Latency (avg.)' } + ); + + case 'p95': + return i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95', + { defaultMessage: 'Latency (95th)' } + ); + + case 'p99': + return i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p99', + { defaultMessage: 'Latency (99th)' } + ); + } +} + +export function getColumns({ + serviceName, + latencyAggregationType, + transactionGroupComparisonStatistics, +}: { + serviceName: string; + latencyAggregationType?: string; + transactionGroupComparisonStatistics?: TransactionGroupComparisonStatistics; +}): Array> { + return [ + { + field: 'name', + sortable: true, + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnName', + { defaultMessage: 'Name' } + ), + render: (_, { name, transactionType: type }) => { + return ( + + {name} + + } + /> + ); + }, + }, + { + field: 'latency', + sortable: true, + name: getLatencyAggregationTypeLabel(latencyAggregationType), + width: px(unit * 10), + render: (_, { latency, name }) => { + const timeseries = + transactionGroupComparisonStatistics?.[name]?.latency; + return ( + + ); + }, + }, + { + field: 'throughput', + sortable: true, + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnThroughput', + { defaultMessage: 'Throughput' } + ), + width: px(unit * 10), + render: (_, { throughput, name }) => { + const timeseries = + transactionGroupComparisonStatistics?.[name]?.throughput; + return ( + + ); + }, + }, + { + field: 'errorRate', + sortable: true, + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnErrorRate', + { defaultMessage: 'Error rate' } + ), + width: px(unit * 8), + render: (_, { errorRate, name }) => { + const timeseries = + transactionGroupComparisonStatistics?.[name]?.errorRate; + return ( + + ); + }, + }, + { + field: 'impact', + sortable: true, + name: i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnImpact', + { defaultMessage: 'Impact' } + ), + width: px(unit * 5), + render: (_, { name }) => { + const impact = + transactionGroupComparisonStatistics?.[name]?.impact ?? 0; + return ; + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index 6ee82f47b9e4..a0facb2ddbed 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -7,86 +7,41 @@ import { EuiBasicTable, - EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { orderBy } from 'lodash'; import React, { useState } from 'react'; -import { ValuesType } from 'utility-types'; -import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; -import { - asMillisecondDuration, - asPercent, - asTransactionRate, -} from '../../../../../common/utils/formatters'; +import uuid from 'uuid'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { px, unit } from '../../../../style/variables'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; -import { ImpactBar } from '../../../shared/ImpactBar'; -import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_link'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; - -type ServiceTransactionGroupItem = ValuesType< - APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/overview'>['transactionGroups'] ->; +import { getColumns } from './get_columns'; interface Props { serviceName: string; } +const INITIAL_STATE = { + transactionGroups: [], + isAggregationAccurate: true, + requestId: '', +}; + type SortField = 'name' | 'latency' | 'throughput' | 'errorRate' | 'impact'; type SortDirection = 'asc' | 'desc'; - const PAGE_SIZE = 5; const DEFAULT_SORT = { direction: 'desc' as const, field: 'impact' as const, }; -function getLatencyAggregationTypeLabel(latencyAggregationType?: string) { - switch (latencyAggregationType) { - case 'avg': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', - { - defaultMessage: 'Latency (avg.)', - } - ); - - case 'p95': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95', - { - defaultMessage: 'Latency (95th)', - } - ); - - case 'p99': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p99', - { - defaultMessage: 'Latency (99th)', - } - ); - } -} - -export function ServiceOverviewTransactionsTable(props: Props) { - const { serviceName } = props; - const { transactionType } = useApmServiceContext(); - const { - uiFilters, - urlParams: { start, end, latencyAggregationType }, - } = useUrlParams(); - +export function ServiceOverviewTransactionsTable({ serviceName }: Props) { const [tableOptions, setTableOptions] = useState<{ pageIndex: number; sort: { @@ -98,51 +53,36 @@ export function ServiceOverviewTransactionsTable(props: Props) { sort: DEFAULT_SORT, }); + const { pageIndex, sort } = tableOptions; + + const { transactionType } = useApmServiceContext(); const { - data = { - totalItemCount: 0, - items: [], - tableOptions: { - pageIndex: 0, - sort: DEFAULT_SORT, - }, - }, - status, - } = useFetcher( + uiFilters, + urlParams: { start, end, latencyAggregationType }, + } = useUrlParams(); + + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end || !latencyAggregationType || !transactionType) { return; } - return callApmApi({ endpoint: - 'GET /api/apm/services/{serviceName}/transactions/groups/overview', + 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics', params: { path: { serviceName }, query: { start, end, uiFilters: JSON.stringify(uiFilters), - size: PAGE_SIZE, - numBuckets: 20, - pageIndex: tableOptions.pageIndex, - sortField: tableOptions.sort.field, - sortDirection: tableOptions.sort.direction, transactionType, - latencyAggregationType: latencyAggregationType as LatencyAggregationType, + latencyAggregationType, }, }, }).then((response) => { return { - items: response.transactionGroups, - totalItemCount: response.totalTransactionGroups, - tableOptions: { - pageIndex: tableOptions.pageIndex, - sort: { - field: tableOptions.sort.field, - direction: tableOptions.sort.direction, - }, - }, + requestId: uuid(), + ...response, }; }); }, @@ -151,114 +91,81 @@ export function ServiceOverviewTransactionsTable(props: Props) { start, end, uiFilters, - tableOptions.pageIndex, - tableOptions.sort.field, - tableOptions.sort.direction, transactionType, latencyAggregationType, ] ); - const { - items, - totalItemCount, - tableOptions: { pageIndex, sort }, - } = data; + const { transactionGroups, requestId } = data; + const currentPageTransactionGroups = orderBy( + transactionGroups, + sort.field, + sort.direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); - const columns: Array> = [ - { - field: 'name', - name: i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnName', - { - defaultMessage: 'Name', - } - ), - render: (_, { name, transactionType: type }) => { - return ( - - {name} - - } - /> - ); - }, + const transactionNames = JSON.stringify( + currentPageTransactionGroups.map(({ name }) => name).sort() + ); + + const { + data: transactionGroupComparisonStatistics, + status: transactionGroupComparisonStatisticsStatus, + } = useFetcher( + (callApmApi) => { + if ( + currentPageTransactionGroups.length && + start && + end && + transactionType && + latencyAggregationType + ) { + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + numBuckets: 20, + transactionType, + latencyAggregationType, + transactionNames, + }, + }, + }); + } }, - { - field: 'latency', - name: getLatencyAggregationTypeLabel(latencyAggregationType), - width: px(unit * 10), - render: (_, { latency }) => { - return ( - - ); - }, + // only fetches statistics when requestId changes or transaction names changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [requestId, transactionNames], + { preservePreviousData: false } + ); + + const columns = getColumns({ + serviceName, + latencyAggregationType, + transactionGroupComparisonStatistics, + }); + + const isLoading = + status === FETCH_STATUS.LOADING || + transactionGroupComparisonStatisticsStatus === FETCH_STATUS.LOADING; + + const pagination = { + pageIndex, + pageSize: PAGE_SIZE, + totalItemCount: transactionGroups.length, + hidePerPageOptions: true, + }; + + const sorting = { + sort: { + field: sort.field, + direction: sort.direction, }, - { - field: 'throughput', - name: i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnThroughput', - { defaultMessage: 'Throughput' } - ), - width: px(unit * 10), - render: (_, { throughput }) => { - return ( - - ); - }, - }, - { - field: 'errorRate', - name: i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnErrorRate', - { - defaultMessage: 'Error rate', - } - ), - width: px(unit * 8), - render: (_, { errorRate }) => { - return ( - - ); - }, - }, - { - field: 'impact', - name: i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnImpact', - { - defaultMessage: 'Impact', - } - ), - width: px(unit * 5), - render: (_, { impact }) => { - return ; - }, - }, - ]; + }; return ( @@ -295,21 +202,14 @@ export function ServiceOverviewTransactionsTable(props: Props) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 1da6bbaad735..36c499f9e5ee 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -16,6 +16,7 @@ import { Settings, } from '@elastic/charts'; import { merge } from 'lodash'; +import { Coordinate } from '../../../../../typings/timeseries'; import { useChartTheme } from '../../../../../../observability/public'; import { px, unit } from '../../../../style/variables'; import { useTheme } from '../../../../hooks/use_theme'; @@ -39,7 +40,7 @@ export function SparkPlot({ compact, }: { color: Color; - series?: Array<{ x: number; y: number | null }> | null; + series?: Coordinate[] | null; valueLabel: React.ReactNode; compact?: boolean; }) { @@ -58,18 +59,18 @@ export function SparkPlot({ const colorValue = theme.eui[color]; + const chartSize = { + height: px(24), + width: compact ? px(unit * 3) : px(unit * 4), + }; + return ( {!series || series.every((point) => point.y === null) ? ( - + ) : ( - + >; diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts index 5522562fbeab..d5974ee3543a 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts @@ -12,7 +12,6 @@ import { useUrlParams } from '../context/url_params_context/use_url_params'; import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; import { getLatencyChartSelector } from '../selectors/latency_chart_selectors'; import { useTheme } from './use_theme'; -import { LatencyAggregationType } from '../../common/latency_aggregation_types'; export function useTransactionLatencyChartsFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); @@ -43,7 +42,7 @@ export function useTransactionLatencyChartsFetcher() { transactionType, transactionName, uiFilters: JSON.stringify(uiFilters), - latencyAggregationType: latencyAggregationType as LatencyAggregationType, + latencyAggregationType, }, }, }); diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts new file mode 100644 index 000000000000..8c21fb65a37e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts @@ -0,0 +1,167 @@ +/* + * 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 { keyBy } from 'lodash'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../common/event_outcome'; +import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { Coordinate } from '../../../typings/timeseries'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; +import { getBucketSize } from '../helpers/get_bucket_size'; +import { + getLatencyAggregation, + getLatencyValue, +} from '../helpers/latency_aggregation_type'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate'; + +export async function getServiceTransactionGroupComparisonStatistics({ + serviceName, + transactionNames, + setup, + numBuckets, + searchAggregatedTransactions, + transactionType, + latencyAggregationType, +}: { + serviceName: string; + transactionNames: string[]; + setup: Setup & SetupTimeRange; + numBuckets: number; + searchAggregatedTransactions: boolean; + transactionType: string; + latencyAggregationType: LatencyAggregationType; +}): Promise< + Record< + string, + { + latency: Coordinate[]; + throughput: Coordinate[]; + errorRate: Coordinate[]; + impact: number; + } + > +> { + const { apmEventClient, start, end, esFilter } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const field = getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ); + + const response = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { range: rangeFilter(start, end) }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ...esFilter, + ], + }, + }, + aggs: { + total_duration: { sum: { field } }, + transaction_groups: { + terms: { + field: TRANSACTION_NAME, + include: transactionNames, + size: transactionNames.length, + }, + aggs: { + transaction_group_total_duration: { + sum: { field }, + }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + throughput_rate: { + rate: { + unit: 'minute', + }, + }, + ...getLatencyAggregation(latencyAggregationType, field), + [EVENT_OUTCOME]: { + terms: { + field: EVENT_OUTCOME, + include: [EventOutcome.failure, EventOutcome.success], + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const buckets = response.aggregations?.transaction_groups.buckets ?? []; + + const totalDuration = response.aggregations?.total_duration.value; + return keyBy( + buckets.map((bucket) => { + const transactionName = bucket.key; + const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: getLatencyValue({ + latencyAggregationType, + aggregation: timeseriesBucket.latency, + }), + })); + const throughput = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: timeseriesBucket.throughput_rate.value, + })); + const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: calculateTransactionErrorPercentage(timeseriesBucket[EVENT_OUTCOME]), + })); + const transactionGroupTotalDuration = + bucket.transaction_group_total_duration.value || 0; + return { + transactionName, + latency, + throughput, + errorRate, + impact: totalDuration + ? (transactionGroupTotalDuration * 100) / totalDuration + : 0, + }; + }), + 'transactionName' + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts similarity index 50% rename from x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts rename to x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts index 9038ddff04e3..67ae37f93606 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts @@ -5,29 +5,27 @@ * 2.0. */ -import { orderBy } from 'lodash'; -import { ValuesType } from 'utility-types'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { EventOutcome } from '../../../../common/event_outcome'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { EVENT_OUTCOME, SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; +} from '../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../common/event_outcome'; +import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { + getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; -import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; +} from '../helpers/aggregated_transactions'; +import { calculateThroughput } from '../helpers/calculate_throughput'; import { getLatencyAggregation, getLatencyValue, -} from '../../helpers/latency_aggregation_type'; -import { calculateThroughput } from '../../helpers/calculate_throughput'; +} from '../helpers/latency_aggregation_type'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate'; export type ServiceOverviewTransactionGroupSortField = | 'name' @@ -36,37 +34,21 @@ export type ServiceOverviewTransactionGroupSortField = | 'errorRate' | 'impact'; -export type TransactionGroupWithoutTimeseriesData = ValuesType< - PromiseReturnType['transactionGroups'] ->; - -export async function getTransactionGroupsForPage({ - apmEventClient, - searchAggregatedTransactions, +export async function getServiceTransactionGroups({ serviceName, - start, - end, - esFilter, - sortField, - sortDirection, - pageIndex, - size, + setup, + searchAggregatedTransactions, transactionType, latencyAggregationType, }: { - apmEventClient: APMEventClient; - searchAggregatedTransactions: boolean; serviceName: string; - start: number; - end: number; - esFilter: ESFilter[]; - sortField: ServiceOverviewTransactionGroupSortField; - sortDirection: 'asc' | 'desc'; - pageIndex: number; - size: number; + setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; transactionType: string; latencyAggregationType: LatencyAggregationType; }) { + const { apmEventClient, start, end, esFilter } = setup; + const field = getTransactionDurationFieldForAggregatedTransactions( searchAggregatedTransactions ); @@ -87,11 +69,15 @@ export async function getTransactionGroupsForPage({ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { range: rangeFilter(start, end) }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ...esFilter, ], }, }, aggs: { + total_duration: { sum: { field } }, transaction_groups: { terms: { field: TRANSACTION_NAME, @@ -99,9 +85,15 @@ export async function getTransactionGroupsForPage({ order: { _count: 'desc' }, }, aggs: { + transaction_group_total_duration: { + sum: { field }, + }, ...getLatencyAggregation(latencyAggregationType, field), [EVENT_OUTCOME]: { - filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + terms: { + field: EVENT_OUTCOME, + include: [EventOutcome.failure, EventOutcome.success], + }, }, }, }, @@ -109,12 +101,16 @@ export async function getTransactionGroupsForPage({ }, }); + const totalDuration = response.aggregations?.total_duration.value; + const transactionGroups = response.aggregations?.transaction_groups.buckets.map((bucket) => { - const errorRate = - bucket.doc_count > 0 - ? bucket[EVENT_OUTCOME].doc_count / bucket.doc_count - : null; + const errorRate = calculateTransactionErrorPercentage( + bucket[EVENT_OUTCOME] + ); + + const transactionGroupTotalDuration = + bucket.transaction_group_total_duration.value || 0; return { name: bucket.key as string, @@ -128,36 +124,17 @@ export async function getTransactionGroupsForPage({ value: bucket.doc_count, }), errorRate, + impact: totalDuration + ? (transactionGroupTotalDuration * 100) / totalDuration + : 0, }; }) ?? []; - const totalDurationValues = transactionGroups.map( - (group) => (group.latency ?? 0) * group.throughput - ); - - const minTotalDuration = Math.min(...totalDurationValues); - const maxTotalDuration = Math.max(...totalDurationValues); - - const transactionGroupsWithImpact = transactionGroups.map((group) => ({ - ...group, - impact: - (((group.latency ?? 0) * group.throughput - minTotalDuration) / - (maxTotalDuration - minTotalDuration)) * - 100, - })); - - // Sort transaction groups first, and only get timeseries for data in view. - // This is to limit the possibility of creating too many buckets. - - const sortedAndSlicedTransactionGroups = orderBy( - transactionGroupsWithImpact, - sortField, - [sortDirection] - ).slice(pageIndex * size, pageIndex * size + size); - return { - transactionGroups: sortedAndSlicedTransactionGroups, - totalTransactionGroups: transactionGroups.length, + transactionGroups: transactionGroups.map((transactionGroup) => ({ + ...transactionGroup, + transactionType, + })), isAggregationAccurate: (response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) === 0, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts deleted file mode 100644 index 21db304c4dfe..000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_timeseries_data_for_transaction_groups.ts +++ /dev/null @@ -1,119 +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 { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { EventOutcome } from '../../../../common/event_outcome'; -import { rangeFilter } from '../../../../common/utils/range_filter'; -import { - EVENT_OUTCOME, - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; - -import { ESFilter } from '../../../../../../typings/elasticsearch'; -import { - getDocumentTypeFilterForAggregatedTransactions, - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; -import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { getLatencyAggregation } from '../../helpers/latency_aggregation_type'; - -export type TransactionGroupTimeseriesData = PromiseReturnType< - typeof getTimeseriesDataForTransactionGroups ->; - -export async function getTimeseriesDataForTransactionGroups({ - apmEventClient, - start, - end, - serviceName, - transactionNames, - esFilter, - searchAggregatedTransactions, - size, - numBuckets, - transactionType, - latencyAggregationType, -}: { - apmEventClient: APMEventClient; - start: number; - end: number; - serviceName: string; - transactionNames: string[]; - esFilter: ESFilter[]; - searchAggregatedTransactions: boolean; - size: number; - numBuckets: number; - transactionType: string; - latencyAggregationType: LatencyAggregationType; -}) { - const { intervalString } = getBucketSize({ start, end, numBuckets }); - - const field = getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ); - - const timeseriesResponse = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { terms: { [TRANSACTION_NAME]: transactionNames } }, - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { range: rangeFilter(start, end) }, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ...esFilter, - ], - }, - }, - aggs: { - transaction_groups: { - terms: { - field: TRANSACTION_NAME, - size, - }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: start, - max: end, - }, - }, - aggs: { - ...getLatencyAggregation(latencyAggregationType, field), - [EVENT_OUTCOME]: { - filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, - }, - }, - }, - }, - }, - }, - }, - }); - - return timeseriesResponse.aggregations?.transaction_groups.buckets ?? []; -} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts deleted file mode 100644 index 3b5426a3c176..000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/index.ts +++ /dev/null @@ -1,89 +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 { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getTimeseriesDataForTransactionGroups } from './get_timeseries_data_for_transaction_groups'; -import { - getTransactionGroupsForPage, - ServiceOverviewTransactionGroupSortField, -} from './get_transaction_groups_for_page'; -import { mergeTransactionGroupData } from './merge_transaction_group_data'; - -export async function getServiceTransactionGroups({ - serviceName, - setup, - size, - numBuckets, - pageIndex, - sortDirection, - sortField, - searchAggregatedTransactions, - transactionType, - latencyAggregationType, -}: { - serviceName: string; - setup: Setup & SetupTimeRange; - size: number; - pageIndex: number; - numBuckets: number; - sortDirection: 'asc' | 'desc'; - sortField: ServiceOverviewTransactionGroupSortField; - searchAggregatedTransactions: boolean; - transactionType: string; - latencyAggregationType: LatencyAggregationType; -}) { - const { apmEventClient, start, end, esFilter } = setup; - - const { - transactionGroups, - totalTransactionGroups, - isAggregationAccurate, - } = await getTransactionGroupsForPage({ - apmEventClient, - start, - end, - serviceName, - esFilter, - pageIndex, - sortField, - sortDirection, - size, - searchAggregatedTransactions, - transactionType, - latencyAggregationType, - }); - - const transactionNames = transactionGroups.map((group) => group.name); - - const timeseriesData = await getTimeseriesDataForTransactionGroups({ - apmEventClient, - start, - end, - esFilter, - numBuckets, - searchAggregatedTransactions, - serviceName, - size, - transactionNames, - transactionType, - latencyAggregationType, - }); - - return { - transactionGroups: mergeTransactionGroupData({ - transactionGroups, - timeseriesData, - start, - end, - latencyAggregationType, - transactionType, - }), - totalTransactionGroups, - isAggregationAccurate, - }; -} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts deleted file mode 100644 index 6d6ad3bf8308..000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts +++ /dev/null @@ -1,90 +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 { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { calculateThroughput } from '../../helpers/calculate_throughput'; -import { getLatencyValue } from '../../helpers/latency_aggregation_type'; -import { TransactionGroupTimeseriesData } from './get_timeseries_data_for_transaction_groups'; -import { TransactionGroupWithoutTimeseriesData } from './get_transaction_groups_for_page'; - -export function mergeTransactionGroupData({ - start, - end, - transactionGroups, - timeseriesData, - latencyAggregationType, - transactionType, -}: { - start: number; - end: number; - transactionGroups: TransactionGroupWithoutTimeseriesData[]; - timeseriesData: TransactionGroupTimeseriesData; - latencyAggregationType: LatencyAggregationType; - transactionType: string; -}) { - return transactionGroups.map((transactionGroup) => { - const groupBucket = timeseriesData.find( - ({ key }) => key === transactionGroup.name - ); - - const timeseriesBuckets = groupBucket?.timeseries.buckets ?? []; - - return timeseriesBuckets.reduce( - (acc, point) => { - return { - ...acc, - latency: { - ...acc.latency, - timeseries: acc.latency.timeseries.concat({ - x: point.key, - y: getLatencyValue({ - latencyAggregationType, - aggregation: point.latency, - }), - }), - }, - throughput: { - ...acc.throughput, - timeseries: acc.throughput.timeseries.concat({ - x: point.key, - y: calculateThroughput({ - start, - end, - value: point.doc_count, - }), - }), - }, - errorRate: { - ...acc.errorRate, - timeseries: acc.errorRate.timeseries.concat({ - x: point.key, - y: point[EVENT_OUTCOME].doc_count / point.doc_count, - }), - }, - }; - }, - { - name: transactionGroup.name, - transactionType, - latency: { - value: transactionGroup.latency, - timeseries: [] as Array<{ x: number; y: number | null }>, - }, - throughput: { - value: transactionGroup.throughput, - timeseries: [] as Array<{ x: number; y: number }>, - }, - errorRate: { - value: transactionGroup.errorRate, - timeseries: [] as Array<{ x: number; y: number | null }>, - }, - impact: transactionGroup.impact, - } - ); - }); -} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 5d580fc0e253..d22bcb1c501e 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -61,9 +61,10 @@ import { transactionChartsDistributionRoute, transactionChartsErrorRateRoute, transactionGroupsRoute, - transactionGroupsOverviewRoute, + transactionGroupsPrimaryStatisticsRoute, transactionLatencyChatsRoute, transactionThroughputChatsRoute, + transactionGroupsComparisonStatisticsRoute, } from './transactions'; import { rumOverviewLocalFiltersRoute, @@ -164,9 +165,10 @@ const createApmApi = () => { .add(transactionChartsDistributionRoute) .add(transactionChartsErrorRateRoute) .add(transactionGroupsRoute) - .add(transactionGroupsOverviewRoute) + .add(transactionGroupsPrimaryStatisticsRoute) .add(transactionLatencyChatsRoute) .add(transactionThroughputChatsRoute) + .add(transactionGroupsComparisonStatisticsRoute) // UI filters .add(uiFiltersEnvironmentsRoute) diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 912820975cad..bef96cb7f076 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -7,27 +7,29 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; -import { createRoute } from './create_route'; -import { rangeRt, uiFiltersRt } from './default_api_types'; -import { toNumberRt } from '../../common/runtime_types/to_number_rt'; -import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; -import { getTransactionBreakdown } from '../lib/transactions/breakdown'; -import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; -import { getTransactionDistribution } from '../lib/transactions/distribution'; -import { getTransactionGroupList } from '../lib/transaction_groups'; -import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; -import { getLatencyTimeseries } from '../lib/transactions/get_latency_charts'; -import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; import { LatencyAggregationType, latencyAggregationTypeRt, } from '../../common/latency_aggregation_types'; +import { jsonRt } from '../../common/runtime_types/json_rt'; +import { toNumberRt } from '../../common/runtime_types/to_number_rt'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; +import { getServiceTransactionGroupComparisonStatistics } from '../lib/services/get_service_transaction_group_comparison_statistics'; +import { getTransactionBreakdown } from '../lib/transactions/breakdown'; +import { getTransactionDistribution } from '../lib/transactions/distribution'; +import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; +import { getLatencyTimeseries } from '../lib/transactions/get_latency_charts'; +import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; +import { getTransactionGroupList } from '../lib/transaction_groups'; +import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; +import { createRoute } from './create_route'; +import { rangeRt, uiFiltersRt } from './default_api_types'; /** * Returns a list of transactions grouped by name - * //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/overview/ + * //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/primary_statistics/ */ export const transactionGroupsRoute = createRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups', @@ -63,25 +65,56 @@ export const transactionGroupsRoute = createRoute({ }, }); -export const transactionGroupsOverviewRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups/overview', +export const transactionGroupsPrimaryStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics', params: t.type({ path: t.type({ serviceName: t.string }), query: t.intersection([ rangeRt, uiFiltersRt, t.type({ - size: toNumberRt, + transactionType: t.string, + latencyAggregationType: latencyAggregationTypeRt, + }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + const { + path: { serviceName }, + query: { latencyAggregationType, transactionType }, + } = context.params; + + return getServiceTransactionGroups({ + setup, + serviceName, + searchAggregatedTransactions, + transactionType, + latencyAggregationType: latencyAggregationType as LatencyAggregationType, + }); + }, +}); + +export const transactionGroupsComparisonStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics', + params: t.type({ + path: t.type({ serviceName: t.string }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + transactionNames: jsonRt, numBuckets: toNumberRt, - pageIndex: toNumberRt, - sortDirection: t.union([t.literal('asc'), t.literal('desc')]), - sortField: t.union([ - t.literal('name'), - t.literal('latency'), - t.literal('throughput'), - t.literal('errorRate'), - t.literal('impact'), - ]), transactionType: t.string, latencyAggregationType: latencyAggregationTypeRt, }), @@ -100,24 +133,18 @@ export const transactionGroupsOverviewRoute = createRoute({ const { path: { serviceName }, query: { + transactionNames, latencyAggregationType, numBuckets, - pageIndex, - size, - sortDirection, - sortField, transactionType, }, } = context.params; - return getServiceTransactionGroups({ + return getServiceTransactionGroupComparisonStatistics({ setup, serviceName, - pageIndex, + transactionNames, searchAggregatedTransactions, - size, - sortDirection, - sortField, transactionType, numBuckets, latencyAggregationType: latencyAggregationType as LatencyAggregationType, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index a59cfab2ba33..72ca22ae749c 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -62,7 +62,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./transactions/latency')); loadTestFile(require.resolve('./transactions/throughput')); loadTestFile(require.resolve('./transactions/top_transaction_groups')); - loadTestFile(require.resolve('./transactions/transactions_groups_overview')); + loadTestFile(require.resolve('./transactions/transactions_groups_primary_statistics')); + loadTestFile(require.resolve('./transactions/transactions_groups_comparison_statistics')); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts index 321ef0c4a763..fde121055181 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts @@ -6,20 +6,17 @@ */ import expect from '@kbn/expect'; +import { last, omit, pick, sortBy } from 'lodash'; import url from 'url'; -import { sortBy, pick, last, omit } from 'lodash'; import { ValuesType } from 'utility-types'; -import { registry } from '../../../common/registry'; -import { Maybe } from '../../../../../plugins/apm/typings/common'; -import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number'; -import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { roundNumber } from '../../../utils'; import { ENVIRONMENT_ALL } from '../../../../../plugins/apm/common/environment_filter_values'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; import archives from '../../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { registry } from '../../../common/registry'; import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; -const round = (num: Maybe): string => (isFiniteNumber(num) ? num.toPrecision(4) : ''); - export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); @@ -235,9 +232,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(opbeansNode !== undefined).to.be(true); const values = { - latency: round(opbeansNode?.latency.value), - throughput: round(opbeansNode?.throughput.value), - errorRate: round(opbeansNode?.errorRate.value), + latency: roundNumber(opbeansNode?.latency.value), + throughput: roundNumber(opbeansNode?.throughput.value), + errorRate: roundNumber(opbeansNode?.errorRate.value), ...pick(opbeansNode, 'serviceName', 'type', 'agentName', 'environment', 'impact'), }; @@ -250,16 +247,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { environment: '', serviceName: 'opbeans-node', type: 'service', - errorRate: round(errors / count), - latency: round(sum / count), - throughput: round(count / ((endTime - startTime) / 1000 / 60)), + errorRate: roundNumber(errors / count), + latency: roundNumber(sum / count), + throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), impact: 100, }); - const firstValue = round(opbeansNode?.latency.timeseries[0].y); - const lastValue = round(last(opbeansNode?.latency.timeseries)?.y); + const firstValue = roundNumber(opbeansNode?.latency.timeseries[0].y); + const lastValue = roundNumber(last(opbeansNode?.latency.timeseries)?.y); - expect(firstValue).to.be(round(20 / 3)); + expect(firstValue).to.be(roundNumber(20 / 3)); expect(lastValue).to.be('1.000'); }); @@ -271,9 +268,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(postgres !== undefined).to.be(true); const values = { - latency: round(postgres?.latency.value), - throughput: round(postgres?.throughput.value), - errorRate: round(postgres?.errorRate.value), + latency: roundNumber(postgres?.latency.value), + throughput: roundNumber(postgres?.throughput.value), + errorRate: roundNumber(postgres?.errorRate.value), ...pick(postgres, 'spanType', 'spanSubtype', 'name', 'impact', 'type'), }; @@ -286,9 +283,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { spanSubtype: 'http', name: 'postgres', type: 'external', - errorRate: round(errors / count), - latency: round(sum / count), - throughput: round(count / ((endTime - startTime) / 1000 / 60)), + errorRate: roundNumber(errors / count), + latency: roundNumber(sum / count), + throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), impact: 0, }); }); diff --git a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap new file mode 100644 index 000000000000..739ff5a080d7 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/transactions_groups_comparison_statistics.snap @@ -0,0 +1,517 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data 1`] = ` +Array [ + Object { + "x": 1607435820000, + "y": null, + }, + Object { + "x": 1607435880000, + "y": 69429, + }, + Object { + "x": 1607435940000, + "y": 8071285, + }, + Object { + "x": 1607436000000, + "y": 31949, + }, + Object { + "x": 1607436060000, + "y": null, + }, + Object { + "x": 1607436120000, + "y": 47755, + }, + Object { + "x": 1607436180000, + "y": null, + }, + Object { + "x": 1607436240000, + "y": 35403, + }, + Object { + "x": 1607436300000, + "y": null, + }, + Object { + "x": 1607436360000, + "y": null, + }, + Object { + "x": 1607436420000, + "y": null, + }, + Object { + "x": 1607436480000, + "y": 48137, + }, + Object { + "x": 1607436540000, + "y": null, + }, + Object { + "x": 1607436600000, + "y": 35457, + }, + Object { + "x": 1607436660000, + "y": null, + }, + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": 30501, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": 46937.5, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data 2`] = ` +Array [ + Object { + "x": 1607435820000, + "y": 0, + }, + Object { + "x": 1607435880000, + "y": 1, + }, + Object { + "x": 1607435940000, + "y": 2, + }, + Object { + "x": 1607436000000, + "y": 1, + }, + Object { + "x": 1607436060000, + "y": 0, + }, + Object { + "x": 1607436120000, + "y": 1, + }, + Object { + "x": 1607436180000, + "y": 0, + }, + Object { + "x": 1607436240000, + "y": 4, + }, + Object { + "x": 1607436300000, + "y": 0, + }, + Object { + "x": 1607436360000, + "y": 0, + }, + Object { + "x": 1607436420000, + "y": 0, + }, + Object { + "x": 1607436480000, + "y": 2, + }, + Object { + "x": 1607436540000, + "y": 0, + }, + Object { + "x": 1607436600000, + "y": 1, + }, + Object { + "x": 1607436660000, + "y": 0, + }, + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 2, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 2, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 0, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data 3`] = ` +Array [ + Object { + "x": 1607435820000, + "y": null, + }, + Object { + "x": 1607435880000, + "y": 0, + }, + Object { + "x": 1607435940000, + "y": 0, + }, + Object { + "x": 1607436000000, + "y": 0, + }, + Object { + "x": 1607436060000, + "y": null, + }, + Object { + "x": 1607436120000, + "y": 0, + }, + Object { + "x": 1607436180000, + "y": null, + }, + Object { + "x": 1607436240000, + "y": 0, + }, + Object { + "x": 1607436300000, + "y": null, + }, + Object { + "x": 1607436360000, + "y": null, + }, + Object { + "x": 1607436420000, + "y": null, + }, + Object { + "x": 1607436480000, + "y": 0, + }, + Object { + "x": 1607436540000, + "y": null, + }, + Object { + "x": 1607436600000, + "y": 0, + }, + Object { + "x": 1607436660000, + "y": null, + }, + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": 0.5, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct for latency aggregation 99th percentile 1`] = ` +Array [ + Object { + "x": 1607435820000, + "y": null, + }, + Object { + "x": 1607435880000, + "y": 69429, + }, + Object { + "x": 1607435940000, + "y": 8198285, + }, + Object { + "x": 1607436000000, + "y": 31949, + }, + Object { + "x": 1607436060000, + "y": null, + }, + Object { + "x": 1607436120000, + "y": 47755, + }, + Object { + "x": 1607436180000, + "y": null, + }, + Object { + "x": 1607436240000, + "y": 73411, + }, + Object { + "x": 1607436300000, + "y": null, + }, + Object { + "x": 1607436360000, + "y": null, + }, + Object { + "x": 1607436420000, + "y": null, + }, + Object { + "x": 1607436480000, + "y": 55116, + }, + Object { + "x": 1607436540000, + "y": null, + }, + Object { + "x": 1607436600000, + "y": 35457, + }, + Object { + "x": 1607436660000, + "y": null, + }, + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": 46040, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": 82486, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts new file mode 100644 index 000000000000..414e2189a63f --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_comparison_statistics.ts @@ -0,0 +1,158 @@ +/* + * 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 url from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { removeEmptyCoordinates, roundNumber } from '../../utils'; + +type TransactionsGroupsComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + const transactionNames = ['DispatcherServlet#doGet', 'APIRestController#customers']; + + registry.when( + 'Transaction groups comparison statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + latencyAggregationType: 'avg', + transactionType: 'request', + transactionNames: JSON.stringify(transactionNames), + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); + + registry.when( + 'Transaction groups comparison statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + latencyAggregationType: 'avg', + transactionNames: JSON.stringify(transactionNames), + }, + }) + ); + + expect(response.status).to.be(200); + + const transactionsGroupsComparisonStatistics = response.body as TransactionsGroupsComparisonStatistics; + + expect(Object.keys(transactionsGroupsComparisonStatistics).length).to.be.eql( + transactionNames.length + ); + + transactionNames.map((transactionName) => { + expect(transactionsGroupsComparisonStatistics[transactionName]).not.to.be.empty(); + }); + + const { latency, throughput, errorRate, impact } = transactionsGroupsComparisonStatistics[ + transactionNames[0] + ]; + + expect(removeEmptyCoordinates(latency).length).to.be.greaterThan(0); + expectSnapshot(latency).toMatch(); + + expect(removeEmptyCoordinates(throughput).length).to.be.greaterThan(0); + expectSnapshot(throughput).toMatch(); + + expect(removeEmptyCoordinates(errorRate).length).to.be.greaterThan(0); + expectSnapshot(errorRate).toMatch(); + + expectSnapshot(roundNumber(impact)).toMatchInline(`"93.93"`); + }); + + it('returns the correct for latency aggregation 99th percentile', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + latencyAggregationType: 'p99', + transactionNames: JSON.stringify(transactionNames), + }, + }) + ); + + expect(response.status).to.be(200); + + const transactionsGroupsComparisonStatistics = response.body as TransactionsGroupsComparisonStatistics; + + expect(Object.keys(transactionsGroupsComparisonStatistics).length).to.be.eql( + transactionNames.length + ); + + transactionNames.map((transactionName) => { + expect(transactionsGroupsComparisonStatistics[transactionName]).not.to.be.empty(); + }); + + const { latency, throughput, errorRate } = transactionsGroupsComparisonStatistics[ + transactionNames[0] + ]; + expect(removeEmptyCoordinates(latency).length).to.be.greaterThan(0); + expectSnapshot(latency).toMatch(); + + expect(removeEmptyCoordinates(throughput).length).to.be.greaterThan(0); + expect(removeEmptyCoordinates(errorRate).length).to.be.greaterThan(0); + }); + + it('returns empty when transaction name is not found', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + latencyAggregationType: 'avg', + transactionNames: JSON.stringify(['foo']), + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_overview.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_overview.ts deleted file mode 100644 index 807373f58286..000000000000 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_overview.ts +++ /dev/null @@ -1,273 +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 { pick, uniqBy, sortBy } from 'lodash'; -import url from 'url'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - registry.when( - 'Transaction groups overview when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`, - query: { - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'impact', - latencyAggregationType: 'avg', - transactionType: 'request', - }, - }) - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - totalTransactionGroups: 0, - transactionGroups: [], - isAggregationAccurate: true, - }); - }); - } - ); - - registry.when( - 'Top transaction groups when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`, - query: { - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'impact', - transactionType: 'request', - latencyAggregationType: 'avg', - }, - }) - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body.totalTransactionGroups).toMatchInline(`12`); - - expectSnapshot(response.body.transactionGroups.map((group: any) => group.name)) - .toMatchInline(` - Array [ - "DispatcherServlet#doGet", - "APIRestController#customers", - "APIRestController#order", - "APIRestController#stats", - "APIRestController#customerWhoBought", - ] - `); - - expectSnapshot(response.body.transactionGroups.map((group: any) => group.impact)) - .toMatchInline(` - Array [ - 100, - 1.43059146953109, - 0.953769516915408, - 0.905498741191481, - 0.894989230293471, - ] - `); - - const firstItem = response.body.transactionGroups[0]; - - expectSnapshot( - pick(firstItem, 'name', 'latency.value', 'throughput.value', 'errorRate.value', 'impact') - ).toMatchInline(` - Object { - "errorRate": Object { - "value": 0.0625, - }, - "impact": 100, - "latency": Object { - "value": 1044995.1875, - }, - "name": "DispatcherServlet#doGet", - "throughput": Object { - "value": 0.533333333333333, - }, - } - `); - - expectSnapshot( - firstItem.latency.timeseries.filter(({ y }: any) => y > 0).length - ).toMatchInline(`9`); - - expectSnapshot( - firstItem.throughput.timeseries.filter(({ y }: any) => y > 0).length - ).toMatchInline(`9`); - - expectSnapshot( - firstItem.errorRate.timeseries.filter(({ y }: any) => y > 0).length - ).toMatchInline(`1`); - }); - - it('sorts items in the correct order', async () => { - const descendingResponse = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`, - query: { - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'impact', - transactionType: 'request', - latencyAggregationType: 'avg', - }, - }) - ); - - expect(descendingResponse.status).to.be(200); - - const descendingOccurrences = descendingResponse.body.transactionGroups.map( - (item: any) => item.impact - ); - - expect(descendingOccurrences).to.eql(sortBy(descendingOccurrences.concat()).reverse()); - - const ascendingResponse = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`, - query: { - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'impact', - transactionType: 'request', - latencyAggregationType: 'avg', - }, - }) - ); - - const ascendingOccurrences = ascendingResponse.body.transactionGroups.map( - (item: any) => item.impact - ); - - expect(ascendingOccurrences).to.eql(sortBy(ascendingOccurrences.concat()).reverse()); - }); - - it('sorts items by the correct field', async () => { - const response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`, - query: { - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'latency', - transactionType: 'request', - latencyAggregationType: 'avg', - }, - }) - ); - - expect(response.status).to.be(200); - - const latencies = response.body.transactionGroups.map((group: any) => group.latency.value); - - expect(latencies).to.eql(sortBy(latencies.concat()).reverse()); - }); - - it('paginates through the items', async () => { - const size = 1; - - const firstPage = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`, - query: { - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'impact', - transactionType: 'request', - latencyAggregationType: 'avg', - }, - }) - ); - - expect(firstPage.status).to.eql(200); - - const totalItems = firstPage.body.totalTransactionGroups; - - const pages = Math.floor(totalItems / size); - - const items = await new Array(pages) - .fill(undefined) - .reduce(async (prevItemsPromise, _, pageIndex) => { - const prevItems = await prevItemsPromise; - - const thisPage = await supertest.get( - url.format({ - pathname: '/api/apm/services/opbeans-java/transactions/groups/overview', - query: { - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex, - sortDirection: 'desc', - sortField: 'impact', - transactionType: 'request', - latencyAggregationType: 'avg', - }, - }) - ); - - return prevItems.concat(thisPage.body.transactionGroups); - }, Promise.resolve([])); - - expect(items.length).to.eql(totalItems); - - expect(uniqBy(items, 'name').length).to.eql(totalItems); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_primary_statistics.ts new file mode 100644 index 000000000000..7d8417bc5bf6 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_primary_statistics.ts @@ -0,0 +1,150 @@ +/* + * 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, sum } from 'lodash'; +import url from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import archives from '../../common/fixtures/es_archiver/archives_metadata'; +import { registry } from '../../common/registry'; + +type TransactionsGroupsPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + registry.when( + 'Transaction groups primary statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + latencyAggregationType: 'avg', + transactionType: 'request', + }, + }) + ); + + expect(response.status).to.be(200); + const transctionsGroupsPrimaryStatistics = response.body as TransactionsGroupsPrimaryStatistics; + expect(transctionsGroupsPrimaryStatistics.transactionGroups).to.empty(); + expect(transctionsGroupsPrimaryStatistics.isAggregationAccurate).to.be(true); + }); + } + ); + + registry.when( + 'Transaction groups primary statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + latencyAggregationType: 'avg', + }, + }) + ); + + expect(response.status).to.be(200); + + const transctionsGroupsPrimaryStatistics = response.body as TransactionsGroupsPrimaryStatistics; + + expectSnapshot( + transctionsGroupsPrimaryStatistics.transactionGroups.map((group: any) => group.name) + ).toMatchInline(` + Array [ + "DispatcherServlet#doGet", + "APIRestController#customerWhoBought", + "APIRestController#order", + "APIRestController#customer", + "ResourceHttpRequestHandler", + "APIRestController#customers", + "APIRestController#stats", + "APIRestController#topProducts", + "APIRestController#orders", + "APIRestController#product", + "APIRestController#products", + "DispatcherServlet#doPost", + ] + `); + + const impacts = transctionsGroupsPrimaryStatistics.transactionGroups.map( + (group: any) => group.impact + ); + expectSnapshot(impacts).toMatchInline(` + Array [ + 93.9295870910491, + 0.850308244392878, + 0.905514602241759, + 0.699947181217412, + 0.143906183235671, + 1.35334507158962, + 0.860178761411346, + 0.476138685202191, + 0.446650726277923, + 0.262571482598846, + 0.062116281544223, + 0.00973568923904662, + ] + `); + + expect(Math.round(sum(impacts))).to.eql(100); + + const firstItem = transctionsGroupsPrimaryStatistics.transactionGroups[0]; + + expectSnapshot(pick(firstItem, 'name', 'latency', 'throughput', 'errorRate', 'impact')) + .toMatchInline(` + Object { + "errorRate": 0.0625, + "impact": 93.9295870910491, + "latency": 1044995.1875, + "name": "DispatcherServlet#doGet", + "throughput": 0.533333333333333, + } + `); + }); + + it('returns the correct data for latency aggregation 99th percentile', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/transactions/groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + latencyAggregationType: 'p99', + }, + }) + ); + + expect(response.status).to.be(200); + + const transctionsGroupsPrimaryStatistics = response.body as TransactionsGroupsPrimaryStatistics; + + const firstItem = transctionsGroupsPrimaryStatistics.transactionGroups[0]; + expectSnapshot(firstItem.latency).toMatchInline(`8198285`); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/utils.ts b/x-pack/test/apm_api_integration/utils.ts new file mode 100644 index 000000000000..0fb99e2aa3c7 --- /dev/null +++ b/x-pack/test/apm_api_integration/utils.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 { Coordinate } from '../../plugins/apm/typings/timeseries'; +import { isFiniteNumber } from '../../plugins/apm/common/utils/is_finite_number'; +import { Maybe } from '../../plugins/apm/typings/common'; + +export function roundNumber(num: Maybe) { + return isFiniteNumber(num) ? num.toPrecision(4) : ''; +} + +export function removeEmptyCoordinates(coordinates: Coordinate[]) { + return coordinates.filter(({ y }) => isFiniteNumber(y)); +} diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index 795205e82aa6..077399c596d5 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -190,6 +190,15 @@ export interface AggregationOptionsByType { gap_policy?: 'skip' | 'insert_zeros'; format?: string; }; + rate: { + unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; + } & ( + | { + field: string; + mode: 'sum' | 'value_count'; + } + | {} + ); } type AggregationType = keyof AggregationOptionsByType; @@ -409,6 +418,9 @@ interface AggregationResponsePart = TFieldName extends string