[APM] Correlations support for progressively loading sections (#95743)

* [APM] Correlations support for progressively loading sections (#95059)

* fixes type consistency

* - Adds progressive section loading for errors tab in correlations
- code improvements

* Tests for latency correlations and overall distribution APIs

* adds API test for error correlations endpoints

* renamed 'getOverallErrorDistribution' to 'getOverallErrorTimeseries'

* Code improvements

* fix whitespace
This commit is contained in:
Oliver Gupte 2021-04-01 13:54:05 -04:00 committed by GitHub
parent 35ba996e84
commit 2abd628f26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 960 additions and 448 deletions

View file

@ -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<CorrelationsApiResponse>['significantTerms']
>[0];
type SignificantTerm = CorrelationsApiResponse['significantTerms'][0];
export type SelectedSignificantTerm = Pick<
SignificantTerm,

View file

@ -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) {
</EuiFlexItem>
<EuiFlexItem>
<ErrorTimeseriesChart
data={hasFieldNames ? data : undefined}
status={status}
overallData={overallData}
correlationsData={hasFieldNames ? correlationsData : undefined}
status={overallStatus}
selectedSignificantTerm={selectedSignificantTerm}
/>
</EuiFlexItem>
@ -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 (
<ChartContainer height={200} hasData={!!data} status={status}>
<ChartContainer height={200} hasData={!!overallData} status={status}>
<Chart size={{ height: px(200), width: '100%' }}>
<Settings showLegend legendPosition={Position.Bottom} />
@ -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 ? (
<LineSeries
id={i18n.translate(
'xpack.apm.correlations.error.chart.selectedTermErrorRateLabel',
@ -227,7 +267,10 @@ function ErrorTimeseriesChart({
xAccessor={'x'}
yAccessors={['y']}
color={theme.eui.euiColorAccent}
data={getSelectedTimeseries(data, selectedSignificantTerm)}
data={getSelectedTimeseries(
correlationsData.significantTerms,
selectedSignificantTerm
)}
curve={CurveType.CURVE_MONOTONE_X}
/>
) : null}

View file

@ -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) {
</h4>
</EuiTitle>
<LatencyDistributionChart
data={hasFieldNames ? data : undefined}
status={status}
overallData={overallData}
correlationsData={
hasFieldNames && correlationsData
? correlationsData?.significantTerms
: undefined
}
status={overallStatus}
selectedSignificantTerm={selectedSignificantTerm}
/>
</EuiFlexItem>
@ -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 (
<ChartContainer height={200} hasData={!!data} status={status}>
<ChartContainer height={200} hasData={!!overallData} status={status}>
<Chart>
<Settings
showLegend
@ -224,7 +272,7 @@ function LatencyDistributionChart({
headerFormatter: (obj) => {
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 ? (
<BarSeries
id={i18n.translate(
'xpack.apm.correlations.latency.chart.selectedTermLatencyDistributionLabel',
@ -276,7 +324,10 @@ function LatencyDistributionChart({
xAccessor={'x'}
yAccessors={['y']}
color={theme.eui.euiColorVis2}
data={getSelectedDistribution(data, selectedSignificantTerm)}
data={getSelectedDistribution(
correlationsData,
selectedSignificantTerm
)}
minBarHeight={5}
tickFormat={(d) => `${roundFloat(d)}%`}
/>

View file

@ -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}`]!;

View file

@ -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
),
},
};
});
}

View file

@ -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<typeof response.aggregations>;
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),
};
}),
};
});
}

View file

@ -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;
}

View file

@ -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 };
});
}

View file

@ -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;
});
}

View file

@ -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<typeof response.aggregations>;
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,
})),
};
});
});
}

View file

@ -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: {

View file

@ -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<T extends { doc_count: number }>(buckets: T[]) {
return dropRightWhile(
buckets,
(bucket, index) => bucket.doc_count === 0 && index > INTERVAL_BUCKETS - 1
);
}

View file

@ -21,6 +21,20 @@ export const getOutcomeAggregation = () => ({
type OutcomeAggregation = ReturnType<typeof getOutcomeAggregation>;
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<OutcomeAggregation, {}>
) {

View file

@ -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({

View file

@ -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

View file

@ -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<ResponseBody>;
};
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,
]
`);
});
}
);
}

View file

@ -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<ResponseBody>;
};
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`);
});
}
);
}

View file

@ -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<ResponseBody>;
};
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`);
});
}
);
}

View file

@ -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<ResponseBody>;
};
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,
]
`);
});
}
);
}

View file

@ -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<ResponseBody>;
};
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`);
});
});
}

View file

@ -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 () {