[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:
parent
35ba996e84
commit
2abd628f26
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)}%`}
|
||||
/>
|
||||
|
|
|
@ -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}`]!;
|
||||
|
|
@ -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
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
56
x-pack/plugins/apm/server/lib/correlations/get_filters.ts
Normal file
56
x-pack/plugins/apm/server/lib/correlations/get_filters.ts
Normal 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;
|
||||
}
|
|
@ -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 };
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
})),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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: {
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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, {}>
|
||||
) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
]
|
||||
`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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 () {
|
||||
|
|
Loading…
Reference in a new issue