diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index 76020d0b4807..75f3cca05c5c 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -24,12 +24,10 @@ import { ImpactBar } from '../../shared/ImpactBar'; import { useUiTracker } from '../../../../../observability/public'; type CorrelationsApiResponse = - | APIReturnType<'GET /api/apm/correlations/failed_transactions'> - | APIReturnType<'GET /api/apm/correlations/slow_transactions'>; + | APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> + | APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; -type SignificantTerm = NonNullable< - NonNullable['significantTerms'] ->[0]; +type SignificantTerm = CorrelationsApiResponse['significantTerms'][0]; export type SelectedSignificantTerm = Pick< SignificantTerm, diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index c3b5f52dd84b..7fb7444a52f8 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -34,8 +34,12 @@ import { useFieldNames } from './use_field_names'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useUiTracker } from '../../../../../observability/public'; +type OverallErrorsApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'> +>; + type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/failed_transactions'> + APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> >; interface Props { @@ -65,11 +69,41 @@ export function ErrorCorrelations({ onClose }: Props) { ); const hasFieldNames = fieldNames.length > 0; - const { data, status } = useFetcher( + const { data: overallData, status: overallStatus } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', + params: { + query: { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + }, + }, + }); + } + }, + [ + environment, + kuery, + serviceName, + start, + end, + transactionName, + transactionType, + ] + ); + + const { data: correlationsData, status: correlationsStatus } = useFetcher( (callApmApi) => { if (start && end && hasFieldNames) { return callApmApi({ - endpoint: 'GET /api/apm/correlations/failed_transactions', + endpoint: 'GET /api/apm/correlations/errors/failed_transactions', params: { query: { environment, @@ -125,8 +159,9 @@ export function ErrorCorrelations({ onClose }: Props) { @@ -136,8 +171,12 @@ export function ErrorCorrelations({ onClose }: Props) { 'xpack.apm.correlations.error.percentageColumnName', { defaultMessage: '% of failed transactions' } )} - significantTerms={hasFieldNames ? data?.significantTerms : []} - status={status} + significantTerms={ + hasFieldNames && correlationsData?.significantTerms + ? correlationsData.significantTerms + : [] + } + status={correlationsStatus} setSelectedSignificantTerm={setSelectedSignificantTerm} onFilter={onClose} /> @@ -151,10 +190,9 @@ export function ErrorCorrelations({ onClose }: Props) { } function getSelectedTimeseries( - data: CorrelationsApiResponse, + significantTerms: CorrelationsApiResponse['significantTerms'], selectedSignificantTerm: SelectedSignificantTerm ) { - const { significantTerms } = data; if (!significantTerms) { return []; } @@ -168,11 +206,13 @@ function getSelectedTimeseries( } function ErrorTimeseriesChart({ - data, + overallData, + correlationsData, selectedSignificantTerm, status, }: { - data?: CorrelationsApiResponse; + overallData?: OverallErrorsApiResponse; + correlationsData?: CorrelationsApiResponse; selectedSignificantTerm: SelectedSignificantTerm | null; status: FETCH_STATUS; }) { @@ -180,7 +220,7 @@ function ErrorTimeseriesChart({ const dateFormatter = timeFormatter('HH:mm:ss'); return ( - + @@ -206,11 +246,11 @@ function ErrorTimeseriesChart({ yScaleType={ScaleType.Linear} xAccessor={'x'} yAccessors={['y']} - data={data?.overall?.timeseries ?? []} + data={overallData?.overall?.timeseries ?? []} curve={CurveType.CURVE_MONOTONE_X} /> - {data && selectedSignificantTerm ? ( + {correlationsData && selectedSignificantTerm ? ( ) : null} diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 77571421ed00..e65bad8088c1 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -32,8 +32,12 @@ import { useFieldNames } from './use_field_names'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useUiTracker } from '../../../../../observability/public'; +type OverallLatencyApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'> +>; + type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/slow_transactions'> + APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'> >; interface Props { @@ -71,11 +75,45 @@ export function LatencyCorrelations({ onClose }: Props) { 75 ); - const { data, status } = useFetcher( + const { data: overallData, status: overallStatus } = useFetcher( (callApmApi) => { - if (start && end && hasFieldNames) { + if (start && end) { return callApmApi({ - endpoint: 'GET /api/apm/correlations/slow_transactions', + endpoint: 'GET /api/apm/correlations/latency/overall_distribution', + params: { + query: { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + }, + }, + }); + } + }, + [ + environment, + kuery, + serviceName, + start, + end, + transactionName, + transactionType, + ] + ); + + const maxLatency = overallData?.maxLatency; + const distributionInterval = overallData?.distributionInterval; + const fieldNamesCommaSeparated = fieldNames.join(','); + + const { data: correlationsData, status: correlationsStatus } = useFetcher( + (callApmApi) => { + if (start && end && hasFieldNames && maxLatency && distributionInterval) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/latency/slow_transactions', params: { query: { environment, @@ -86,7 +124,9 @@ export function LatencyCorrelations({ onClose }: Props) { start, end, durationPercentile: durationPercentile.toString(10), - fieldNames: fieldNames.join(','), + fieldNames: fieldNamesCommaSeparated, + maxLatency: maxLatency.toString(10), + distributionInterval: distributionInterval.toString(10), }, }, }); @@ -101,8 +141,10 @@ export function LatencyCorrelations({ onClose }: Props) { transactionName, transactionType, durationPercentile, - fieldNames, + fieldNamesCommaSeparated, hasFieldNames, + maxLatency, + distributionInterval, ] ); @@ -134,8 +176,13 @@ export function LatencyCorrelations({ onClose }: Props) { @@ -147,8 +194,12 @@ export function LatencyCorrelations({ onClose }: Props) { 'xpack.apm.correlations.latency.percentageColumnName', { defaultMessage: '% of slow transactions' } )} - significantTerms={hasFieldNames ? data?.significantTerms : []} - status={status} + significantTerms={ + hasFieldNames && correlationsData + ? correlationsData?.significantTerms + : [] + } + status={correlationsStatus} setSelectedSignificantTerm={setSelectedSignificantTerm} onFilter={onClose} /> @@ -167,25 +218,23 @@ export function LatencyCorrelations({ onClose }: Props) { ); } -function getDistributionYMax(data?: CorrelationsApiResponse) { - if (!data?.overall) { - return 0; +function getAxisMaxes(data?: OverallLatencyApiResponse) { + if (!data?.overallDistribution) { + return { xMax: 0, yMax: 0 }; } - - const yValues = [ - ...data.overall.distribution.map((p) => p.y ?? 0), - ...data.significantTerms.flatMap((term) => - term.distribution.map((p) => p.y ?? 0) - ), - ]; - return Math.max(...yValues); + const { overallDistribution } = data; + const xValues = overallDistribution.map((p) => p.x ?? 0); + const yValues = overallDistribution.map((p) => p.y ?? 0); + return { + xMax: Math.max(...xValues), + yMax: Math.max(...yValues), + }; } function getSelectedDistribution( - data: CorrelationsApiResponse, + significantTerms: CorrelationsApiResponse['significantTerms'], selectedSignificantTerm: SelectedSignificantTerm ) { - const { significantTerms } = data; if (!significantTerms) { return []; } @@ -199,23 +248,22 @@ function getSelectedDistribution( } function LatencyDistributionChart({ - data, + overallData, + correlationsData, selectedSignificantTerm, status, }: { - data?: CorrelationsApiResponse; + overallData?: OverallLatencyApiResponse; + correlationsData?: CorrelationsApiResponse['significantTerms']; selectedSignificantTerm: SelectedSignificantTerm | null; status: FETCH_STATUS; }) { const theme = useTheme(); - const xMax = Math.max( - ...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? []) - ); + const { xMax, yMax } = getAxisMaxes(overallData); const durationFormatter = getDurationFormatter(xMax); - const yMax = getDistributionYMax(data); return ( - + { const start = durationFormatter(obj.value); const end = durationFormatter( - obj.value + data?.distributionInterval + obj.value + overallData?.distributionInterval ); return `${start.value} - ${end.formatted}`; @@ -254,12 +302,12 @@ function LatencyDistributionChart({ xAccessor={'x'} yAccessors={['y']} color={theme.eui.euiColorVis1} - data={data?.overall?.distribution || []} + data={overallData?.overallDistribution || []} minBarHeight={5} tickFormat={(d) => `${roundFloat(d)}%`} /> - {data && selectedSignificantTerm ? ( + {correlationsData && selectedSignificantTerm ? ( `${roundFloat(d)}%`} /> diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts similarity index 63% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts rename to x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts index c668f3bb2871..8ee469c9a93c 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty, omit, merge } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { EventOutcome } from '../../../../common/event_outcome'; import { processSignificantTermAggs, @@ -13,65 +13,25 @@ import { } from '../process_significant_term_aggs'; import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; import { ESFilter } from '../../../../../../../typings/elasticsearch'; -import { - environmentQuery, - rangeQuery, - kqlQuery, -} from '../../../../server/utils/queries'; -import { - EVENT_OUTCOME, - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { - getOutcomeAggregation, + getTimeseriesAggregation, getTransactionErrorRateTimeSeries, } from '../../helpers/transaction_error_rate'; import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; -export async function getCorrelationsForFailedTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - fieldNames, - setup, -}: { - environment?: string; - kuery?: string; - serviceName: string | undefined; - transactionType: string | undefined; - transactionName: string | undefined; +interface Options extends CorrelationsOptions { fieldNames: string[]; - setup: Setup & SetupTimeRange; -}) { +} +export async function getCorrelationsForFailedTransactions(options: Options) { return withApmSpan('get_correlations_for_failed_transactions', async () => { - const { start, end, apmEventClient } = setup; - - const backgroundFilters: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (serviceName) { - backgroundFilters.push({ term: { [SERVICE_NAME]: serviceName } }); - } - - if (transactionType) { - backgroundFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - - if (transactionName) { - backgroundFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } + const { fieldNames, setup } = options; + const { apmEventClient } = setup; + const filters = getCorrelationsFilters(options); const params = { apm: { events: [ProcessorEvent.transaction] }, @@ -79,7 +39,7 @@ export async function getCorrelationsForFailedTransactions({ body: { size: 0, query: { - bool: { filter: backgroundFilters }, + bool: { filter: filters }, }, aggs: { failed_transactions: { @@ -95,7 +55,7 @@ export async function getCorrelationsForFailedTransactions({ field: fieldName, background_filter: { bool: { - filter: backgroundFilters, + filter: filters, must_not: { term: { [EVENT_OUTCOME]: EventOutcome.failure }, }, @@ -112,7 +72,7 @@ export async function getCorrelationsForFailedTransactions({ const response = await apmEventClient.search(params); if (!response.aggregations) { - return {}; + return { significantTerms: [] }; } const sigTermAggs = omit( @@ -121,17 +81,17 @@ export async function getCorrelationsForFailedTransactions({ ); const topSigTerms = processSignificantTermAggs({ sigTermAggs }); - return getErrorRateTimeSeries({ setup, backgroundFilters, topSigTerms }); + return getErrorRateTimeSeries({ setup, filters, topSigTerms }); }); } export async function getErrorRateTimeSeries({ setup, - backgroundFilters, + filters, topSigTerms, }: { setup: Setup & SetupTimeRange; - backgroundFilters: ESFilter[]; + filters: ESFilter[]; topSigTerms: TopSigTerm[]; }) { return withApmSpan('get_error_rate_timeseries', async () => { @@ -139,20 +99,10 @@ export async function getErrorRateTimeSeries({ const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); if (isEmpty(topSigTerms)) { - return {}; + return { significantTerms: [] }; } - const timeseriesAgg = { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }, - aggs: { - outcomes: getOutcomeAggregation(), - }, - }; + const timeseriesAgg = getTimeseriesAggregation(start, end, intervalString); const perTermAggs = topSigTerms.reduce( (acc, term, index) => { @@ -175,8 +125,8 @@ export async function getErrorRateTimeSeries({ apm: { events: [ProcessorEvent.transaction] }, body: { size: 0, - query: { bool: { filter: backgroundFilters } }, - aggs: merge({ timeseries: timeseriesAgg }, perTermAggs), + query: { bool: { filter: filters } }, + aggs: perTermAggs, }, }; @@ -184,15 +134,10 @@ export async function getErrorRateTimeSeries({ const { aggregations } = response; if (!aggregations) { - return {}; + return { significantTerms: [] }; } return { - overall: { - timeseries: getTransactionErrorRateTimeSeries( - aggregations.timeseries.buckets - ), - }, significantTerms: topSigTerms.map((topSig, index) => { const agg = aggregations[`term_${index}`]!; diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts new file mode 100644 index 000000000000..9387e64a51e0 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + getTimeseriesAggregation, + getTransactionErrorRateTimeSeries, +} from '../../helpers/transaction_error_rate'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; + +export async function getOverallErrorTimeseries(options: CorrelationsOptions) { + return withApmSpan('get_error_rate_timeseries', async () => { + const { setup } = options; + const filters = getCorrelationsFilters(options); + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + timeseries: getTimeseriesAggregation(start, end, intervalString), + }, + }, + }; + + const response = await apmEventClient.search(params); + const { aggregations } = response; + + if (!aggregations) { + return { overall: null }; + } + + return { + overall: { + timeseries: getTransactionErrorRateTimeSeries( + aggregations.timeseries.buckets + ), + }, + }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts deleted file mode 100644 index 88b1cf3a344e..000000000000 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts +++ /dev/null @@ -1,143 +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 { isEmpty, dropRightWhile } from 'lodash'; -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { TopSigTerm } from '../process_significant_term_aggs'; -import { getMaxLatency } from './get_max_latency'; -import { withApmSpan } from '../../../utils/with_apm_span'; - -export async function getLatencyDistribution({ - setup, - backgroundFilters, - topSigTerms, -}: { - setup: Setup & SetupTimeRange; - backgroundFilters: ESFilter[]; - topSigTerms: TopSigTerm[]; -}) { - return withApmSpan('get_latency_distribution', async () => { - const { apmEventClient } = setup; - - if (isEmpty(topSigTerms)) { - return {}; - } - - const maxLatency = await getMaxLatency({ - setup, - backgroundFilters, - topSigTerms, - }); - - if (!maxLatency) { - return {}; - } - - const intervalBuckets = 15; - const distributionInterval = Math.floor(maxLatency / intervalBuckets); - - const distributionAgg = { - // filter out outliers not included in the significant term docs - filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } }, - aggs: { - dist_filtered_by_latency: { - histogram: { - // TODO: add support for metrics - field: TRANSACTION_DURATION, - interval: distributionInterval, - min_doc_count: 0, - extended_bounds: { - min: 0, - max: maxLatency, - }, - }, - }, - }, - }; - - const perTermAggs = topSigTerms.reduce( - (acc, term, index) => { - acc[`term_${index}`] = { - filter: { term: { [term.fieldName]: term.fieldValue } }, - aggs: { - distribution: distributionAgg, - }, - }; - return acc; - }, - {} as Record< - string, - { - filter: AggregationOptionsByType['filter']; - aggs: { - distribution: typeof distributionAgg; - }; - } - > - ); - - const params = { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { bool: { filter: backgroundFilters } }, - aggs: { - // overall aggs - distribution: distributionAgg, - - // per term aggs - ...perTermAggs, - }, - }, - }; - - const response = await withApmSpan('get_terms_distribution', () => - apmEventClient.search(params) - ); - type Agg = NonNullable; - - if (!response.aggregations) { - return {}; - } - - function formatDistribution(distribution: Agg['distribution']) { - const total = distribution.doc_count; - - // remove trailing buckets that are empty and out of bounds of the desired number of buckets - const buckets = dropRightWhile( - distribution.dist_filtered_by_latency.buckets, - (bucket, index) => bucket.doc_count === 0 && index > intervalBuckets - 1 - ); - - return buckets.map((bucket) => ({ - x: bucket.key, - y: (bucket.doc_count / total) * 100, - })); - } - - return { - distributionInterval, - overall: { - distribution: formatDistribution(response.aggregations.distribution), - }, - significantTerms: topSigTerms.map((topSig, index) => { - // @ts-expect-error - const agg = response.aggregations[`term_${index}`] as Agg; - - return { - ...topSig, - distribution: formatDistribution(agg.distribution), - }; - }), - }; - }); -} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts new file mode 100644 index 000000000000..92fc9c5d9622 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { environmentQuery, rangeQuery, kqlQuery } from '../../utils/queries'; +import { + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; + +export interface CorrelationsOptions { + setup: Setup & SetupTimeRange; + environment?: string; + kuery?: string; + serviceName: string | undefined; + transactionType: string | undefined; + transactionName: string | undefined; +} + +export function getCorrelationsFilters({ + setup, + environment, + kuery, + serviceName, + transactionType, + transactionName, +}: CorrelationsOptions) { + const { start, end } = setup; + const correlationsFilters: ESFilter[] = [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ]; + + if (serviceName) { + correlationsFilters.push({ term: { [SERVICE_NAME]: serviceName } }); + } + + if (transactionType) { + correlationsFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); + } + + if (transactionName) { + correlationsFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); + } + return correlationsFilters; +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts similarity index 63% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts rename to x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts index 9472d385a26c..0f93d1411a00 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts @@ -6,75 +6,39 @@ */ import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; -import { - environmentQuery, - rangeQuery, - kqlQuery, -} from '../../../../server/utils/queries'; -import { - SERVICE_NAME, - TRANSACTION_DURATION, - TRANSACTION_NAME, - TRANSACTION_TYPE, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getDurationForPercentile } from './get_duration_for_percentile'; import { processSignificantTermAggs } from '../process_significant_term_aggs'; import { getLatencyDistribution } from './get_latency_distribution'; import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; -export async function getCorrelationsForSlowTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - durationPercentile, - fieldNames, - setup, -}: { - environment?: string; - kuery?: string; - serviceName: string | undefined; - transactionType: string | undefined; - transactionName: string | undefined; +interface Options extends CorrelationsOptions { durationPercentile: number; fieldNames: string[]; - setup: Setup & SetupTimeRange; -}) { + maxLatency: number; + distributionInterval: number; +} +export async function getCorrelationsForSlowTransactions(options: Options) { return withApmSpan('get_correlations_for_slow_transactions', async () => { - const { start, end, apmEventClient } = setup; - - const backgroundFilters: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (serviceName) { - backgroundFilters.push({ term: { [SERVICE_NAME]: serviceName } }); - } - - if (transactionType) { - backgroundFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - - if (transactionName) { - backgroundFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - + const { + durationPercentile, + fieldNames, + setup, + maxLatency, + distributionInterval, + } = options; + const { apmEventClient } = setup; + const filters = getCorrelationsFilters(options); const durationForPercentile = await getDurationForPercentile({ durationPercentile, - backgroundFilters, + filters, setup, }); if (!durationForPercentile) { - return {}; + return { significantTerms: [] }; } const response = await withApmSpan('get_significant_terms', () => { @@ -85,7 +49,7 @@ export async function getCorrelationsForSlowTransactions({ query: { bool: { // foreground filters - filter: backgroundFilters, + filter: filters, must: { function_score: { query: { @@ -112,7 +76,7 @@ export async function getCorrelationsForSlowTransactions({ background_filter: { bool: { filter: [ - ...backgroundFilters, + ...filters, { range: { [TRANSACTION_DURATION]: { @@ -132,17 +96,21 @@ export async function getCorrelationsForSlowTransactions({ return apmEventClient.search(params); }); if (!response.aggregations) { - return {}; + return { significantTerms: [] }; } const topSigTerms = processSignificantTermAggs({ sigTermAggs: response.aggregations, }); - return getLatencyDistribution({ + const significantTerms = await getLatencyDistribution({ setup, - backgroundFilters, + filters, topSigTerms, + maxLatency, + distributionInterval, }); + + return { significantTerms }; }); } diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts similarity index 86% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts rename to x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts index 02141f5f9e76..43c261743861 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts @@ -13,11 +13,11 @@ import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getDurationForPercentile({ durationPercentile, - backgroundFilters, + filters, setup, }: { durationPercentile: number; - backgroundFilters: ESFilter[]; + filters: ESFilter[]; setup: Setup & SetupTimeRange; }) { return withApmSpan('get_duration_for_percentiles', async () => { @@ -29,7 +29,7 @@ export async function getDurationForPercentile({ body: { size: 0, query: { - bool: { filter: backgroundFilters }, + bool: { filter: filters }, }, aggs: { percentile: { @@ -42,6 +42,9 @@ export async function getDurationForPercentile({ }, }); - return Object.values(res.aggregations?.percentile.values || {})[0]; + const duration = Object.values( + res.aggregations?.percentile.values || {} + )[0]; + return duration || 0; }); } diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts new file mode 100644 index 000000000000..6d42b26b22e4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts @@ -0,0 +1,98 @@ +/* + * 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 { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { TopSigTerm } from '../process_significant_term_aggs'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { + getDistributionAggregation, + trimBuckets, +} from './get_overall_latency_distribution'; + +export async function getLatencyDistribution({ + setup, + filters, + topSigTerms, + maxLatency, + distributionInterval, +}: { + setup: Setup & SetupTimeRange; + filters: ESFilter[]; + topSigTerms: TopSigTerm[]; + maxLatency: number; + distributionInterval: number; +}) { + return withApmSpan('get_latency_distribution', async () => { + const { apmEventClient } = setup; + + const distributionAgg = getDistributionAggregation( + maxLatency, + distributionInterval + ); + + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { + distribution: distributionAgg, + }, + }; + return acc; + }, + {} as Record< + string, + { + filter: AggregationOptionsByType['filter']; + aggs: { + distribution: typeof distributionAgg; + }; + } + > + ); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: perTermAggs, + }, + }; + + const response = await withApmSpan('get_terms_distribution', () => + apmEventClient.search(params) + ); + type Agg = NonNullable; + + if (!response.aggregations) { + return []; + } + + return topSigTerms.map((topSig, index) => { + // ignore the typescript error since existence of response.aggregations is already checked: + // @ts-expect-error + const agg = response.aggregations[`term_${index}`] as Agg[string]; + const total = agg.distribution.doc_count; + const buckets = trimBuckets( + agg.distribution.dist_filtered_by_latency.buckets + ); + + return { + ...topSig, + distribution: buckets.map((bucket) => ({ + x: bucket.key, + y: (bucket.doc_count / total) * 100, + })), + }; + }); + }); +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts similarity index 76% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts rename to x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts index 5f12c86a9c70..8b415bf0d80a 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts @@ -14,12 +14,12 @@ import { TopSigTerm } from '../process_significant_term_aggs'; export async function getMaxLatency({ setup, - backgroundFilters, - topSigTerms, + filters, + topSigTerms = [], }: { setup: Setup & SetupTimeRange; - backgroundFilters: ESFilter[]; - topSigTerms: TopSigTerm[]; + filters: ESFilter[]; + topSigTerms?: TopSigTerm[]; }) { return withApmSpan('get_max_latency', async () => { const { apmEventClient } = setup; @@ -31,13 +31,17 @@ export async function getMaxLatency({ size: 0, query: { bool: { - filter: backgroundFilters, + filter: filters, - // only include docs containing the significant terms - should: topSigTerms.map((term) => ({ - term: { [term.fieldName]: term.fieldValue }, - })), - minimum_should_match: 1, + ...(topSigTerms.length + ? { + // only include docs containing the significant terms + should: topSigTerms.map((term) => ({ + term: { [term.fieldName]: term.fieldValue }, + })), + minimum_should_match: 1, + } + : null), }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts new file mode 100644 index 000000000000..c5d4def51ea5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts @@ -0,0 +1,107 @@ +/* + * 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 { dropRightWhile } from 'lodash'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getMaxLatency } from './get_max_latency'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; + +export const INTERVAL_BUCKETS = 15; + +export function getDistributionAggregation( + maxLatency: number, + distributionInterval: number +) { + return { + filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } }, + aggs: { + dist_filtered_by_latency: { + histogram: { + // TODO: add support for metrics + field: TRANSACTION_DURATION, + interval: distributionInterval, + min_doc_count: 0, + extended_bounds: { + min: 0, + max: maxLatency, + }, + }, + }, + }, + }; +} + +export async function getOverallLatencyDistribution( + options: CorrelationsOptions +) { + const { setup } = options; + const filters = getCorrelationsFilters(options); + + return withApmSpan('get_overall_latency_distribution', async () => { + const { apmEventClient } = setup; + const maxLatency = await getMaxLatency({ setup, filters }); + if (!maxLatency) { + return { + maxLatency: null, + distributionInterval: null, + overallDistribution: null, + }; + } + const distributionInterval = Math.floor(maxLatency / INTERVAL_BUCKETS); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + // overall distribution agg + distribution: getDistributionAggregation( + maxLatency, + distributionInterval + ), + }, + }, + }; + + const response = await withApmSpan('get_terms_distribution', () => + apmEventClient.search(params) + ); + + if (!response.aggregations) { + return { + maxLatency, + distributionInterval, + overallDistribution: null, + }; + } + + const { distribution } = response.aggregations; + const total = distribution.doc_count; + const buckets = trimBuckets(distribution.dist_filtered_by_latency.buckets); + + return { + maxLatency, + distributionInterval, + overallDistribution: buckets.map((bucket) => ({ + x: bucket.key, + y: (bucket.doc_count / total) * 100, + })), + }; + }); +} + +// remove trailing buckets that are empty and out of bounds of the desired number of buckets +export function trimBuckets(buckets: T[]) { + return dropRightWhile( + buckets, + (bucket, index) => bucket.doc_count === 0 && index > INTERVAL_BUCKETS - 1 + ); +} diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 11d65b7697e9..b60a2a071e6d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -21,6 +21,20 @@ export const getOutcomeAggregation = () => ({ type OutcomeAggregation = ReturnType; +export const getTimeseriesAggregation = ( + start: number, + end: number, + intervalString: string +) => ({ + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { outcomes: getOutcomeAggregation() }, +}); + export function calculateTransactionErrorPercentage( outcomeResponse: AggregationResultOf ) { diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 48305d1a9df0..c7c69e077482 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -9,8 +9,10 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; import * as t from 'io-ts'; import { isActivePlatinumLicense } from '../../common/license_check'; -import { getCorrelationsForFailedTransactions } from '../lib/correlations/get_correlations_for_failed_transactions'; -import { getCorrelationsForSlowTransactions } from '../lib/correlations/get_correlations_for_slow_transactions'; +import { getCorrelationsForFailedTransactions } from '../lib/correlations/errors/get_correlations_for_failed_transactions'; +import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overall_error_timeseries'; +import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions'; +import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution'; import { setupRequest } from '../lib/helpers/setup_request'; import { createRoute } from './create_route'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; @@ -23,8 +25,47 @@ const INVALID_LICENSE = i18n.translate( } ); +export const correlationsLatencyDistributionRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/latency/overall_distribution', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + const setup = await setupRequest(context, request); + const { + environment, + kuery, + serviceName, + transactionType, + transactionName, + } = context.params.query; + + return getOverallLatencyDistribution({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, + }); + }, +}); + export const correlationsForSlowTransactionsRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/slow_transactions', + endpoint: 'GET /api/apm/correlations/latency/slow_transactions', params: t.type({ query: t.intersection([ t.partial({ @@ -35,6 +76,8 @@ export const correlationsForSlowTransactionsRoute = createRoute({ t.type({ durationPercentile: t.string, fieldNames: t.string, + maxLatency: t.string, + distributionInterval: t.string, }), environmentRt, kueryRt, @@ -55,6 +98,8 @@ export const correlationsForSlowTransactionsRoute = createRoute({ transactionName, durationPercentile, fieldNames, + maxLatency, + distributionInterval, } = context.params.query; return getCorrelationsForSlowTransactions({ @@ -66,12 +111,53 @@ export const correlationsForSlowTransactionsRoute = createRoute({ durationPercentile: parseInt(durationPercentile, 10), fieldNames: fieldNames.split(','), setup, + maxLatency: parseInt(maxLatency, 10), + distributionInterval: parseInt(distributionInterval, 10), + }); + }, +}); + +export const correlationsErrorDistributionRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + const setup = await setupRequest(context, request); + const { + environment, + kuery, + serviceName, + transactionType, + transactionName, + } = context.params.query; + + return getOverallErrorTimeseries({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, }); }, }); export const correlationsForFailedTransactionsRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/failed_transactions', + endpoint: 'GET /api/apm/correlations/errors/failed_transactions', params: t.type({ query: t.intersection([ t.partial({ 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 2b5fb0b516ab..5b74aa4347f1 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -58,7 +58,9 @@ import { rootTransactionByTraceIdRoute, } from './traces'; import { + correlationsLatencyDistributionRoute, correlationsForSlowTransactionsRoute, + correlationsErrorDistributionRoute, correlationsForFailedTransactionsRoute, } from './correlations'; import { @@ -152,7 +154,9 @@ const createApmApi = () => { .add(createOrUpdateAgentConfigurationRoute) // Correlations + .add(correlationsLatencyDistributionRoute) .add(correlationsForSlowTransactionsRoute) + .add(correlationsErrorDistributionRoute) .add(correlationsForFailedTransactionsRoute) // APM indices diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts new file mode 100644 index 000000000000..80c2b9826624 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts @@ -0,0 +1,82 @@ +/* + * 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 { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/errors/failed_transactions`, + query: { + start: range.start, + end: range.end, + fieldNames: 'user_agent.name,user_agent.os.name,url.original', + }, + }); + registry.when( + 'correlations errors failed transactions without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations errors failed transactions with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns significant terms', () => { + const { significantTerms } = response.body; + expect(significantTerms).to.have.length(2); + const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); + expectSnapshot(sortedFieldNames).toMatchInline(` + Array [ + "user_agent.name", + "user_agent.name", + ] + `); + }); + + it('returns a distribution per term', () => { + const { significantTerms } = response.body; + expectSnapshot(significantTerms.map((term) => term.timeseries.length)).toMatchInline(` + Array [ + 31, + 31, + ] + `); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts new file mode 100644 index 000000000000..206da2968b4c --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts @@ -0,0 +1,64 @@ +/* + * 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 { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/errors/overall_timeseries`, + query: { + start: range.start, + end: range.end, + }, + }); + + registry.when( + 'correlations errors overall without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations errors overall with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns overall distribution', () => { + expectSnapshot(response.body?.overall?.timeseries.length).toMatchInline(`31`); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts new file mode 100644 index 000000000000..0d79333faa9e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts @@ -0,0 +1,66 @@ +/* + * 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 { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/latency/overall_distribution`, + query: { + start: range.start, + end: range.end, + }, + }); + + registry.when( + 'correlations latency overall without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations latency overall with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns overall distribution', () => { + expectSnapshot(response.body?.distributionInterval).toMatchInline(`238776`); + expectSnapshot(response.body?.maxLatency).toMatchInline(`3581640.00000003`); + expectSnapshot(response.body?.overallDistribution?.length).toMatchInline(`15`); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts new file mode 100644 index 000000000000..d32beee0f31d --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts @@ -0,0 +1,99 @@ +/* + * 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 { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/latency/slow_transactions`, + query: { + start: range.start, + end: range.end, + durationPercentile: 95, + fieldNames: 'user_agent.name,user_agent.os.name,url.original', + maxLatency: 3581640.00000003, + distributionInterval: 238776, + }, + }); + registry.when( + 'correlations latency slow transactions without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations latency slow transactions with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns significant terms', () => { + const { significantTerms } = response.body; + expect(significantTerms).to.have.length(9); + const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); + expectSnapshot(sortedFieldNames).toMatchInline(` + Array [ + "url.original", + "url.original", + "url.original", + "url.original", + "user_agent.name", + "user_agent.name", + "user_agent.name", + "user_agent.os.name", + "user_agent.os.name", + ] + `); + }); + + it('returns a distribution per term', () => { + const { significantTerms } = response.body; + expectSnapshot(significantTerms.map((term) => term.distribution.length)).toMatchInline(` + Array [ + 15, + 15, + 15, + 15, + 15, + 15, + 15, + 15, + 15, + ] + `); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts deleted file mode 100644 index c9686a8a9d5b..000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/slow_transactions`, - query: { - start: range.start, - end: range.end, - durationPercentile: 95, - fieldNames: 'user_agent.name,user_agent.os.name,url.original', - }, - }); - - registry.when('without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - }); - - registry.when('with data and default args', { config: 'trial', archives: ['apm_8.0.0'] }, () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/slow_transactions'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns significant terms', () => { - const significantTerms = response.body?.significantTerms as NonNullable< - typeof response.body.significantTerms - >; - expect(significantTerms).to.have.length(9); - const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); - expectSnapshot(sortedFieldNames).toMatchInline(` - Array [ - "url.original", - "url.original", - "url.original", - "url.original", - "user_agent.name", - "user_agent.name", - "user_agent.name", - "user_agent.os.name", - "user_agent.os.name", - ] - `); - }); - - it('returns a distribution per term', () => { - expectSnapshot(response.body?.significantTerms?.map((term) => term.distribution.length)) - .toMatchInline(` - Array [ - 15, - 15, - 15, - 15, - 15, - 15, - 15, - 15, - 15, - ] - `); - }); - - it('returns overall distribution', () => { - expectSnapshot(response.body?.overall?.distribution.length).toMatchInline(`15`); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 9f0f1b15c058..7c69d5b996ce 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -24,8 +24,20 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./alerts/chart_preview')); }); - describe('correlations/slow_transactions', function () { - loadTestFile(require.resolve('./correlations/slow_transactions')); + describe('correlations/latency_slow_transactions', function () { + loadTestFile(require.resolve('./correlations/latency_slow_transactions')); + }); + + describe('correlations/latency_overall', function () { + loadTestFile(require.resolve('./correlations/latency_overall')); + }); + + describe('correlations/errors_overall', function () { + loadTestFile(require.resolve('./correlations/errors_overall')); + }); + + describe('correlations/errors_failed_transactions', function () { + loadTestFile(require.resolve('./correlations/errors_failed_transactions')); }); describe('metrics_charts/metrics_charts', function () {