[APM] Adding comparison latency chart (#91339)

* adding time comparison to latency chart

* adding time comparison to latency chart

* fixing TS

* fixing api test

* addressing PR comments

* adding api test

* addressing PR comments

* fixing api test

* rounding date diff

* addressing PR comments

* fixing api test

* refactoring

* fixing ts issue

* fixing offset function

* fixing offset function

* addressing PR comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2021-03-10 11:02:50 -05:00 committed by GitHub
parent 2bb23291c7
commit f428b10a79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 690 additions and 287 deletions

View file

@ -13,6 +13,7 @@ import { LatencyAggregationType } from '../../../../../common/latency_aggregatio
import { getDurationFormatter } from '../../../../../common/utils/formatters';
import { useLicenseContext } from '../../../../context/license/use_license_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useTheme } from '../../../../hooks/use_theme';
import { useTransactionLatencyChartsFetcher } from '../../../../hooks/use_transaction_latency_chart_fetcher';
import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
import {
@ -21,6 +22,7 @@ import {
} from '../../../shared/charts/transaction_charts/helper';
import { MLHeader } from '../../../shared/charts/transaction_charts/ml_header';
import * as urlHelpers from '../../../shared/Links/url_helpers';
import { getComparisonChartTheme } from '../../time_comparison/get_time_range_comparison';
interface Props {
height?: number;
@ -32,10 +34,16 @@ const options: Array<{ value: LatencyAggregationType; text: string }> = [
{ value: LatencyAggregationType.p99, text: '99th percentile' },
];
function filterNil<T>(value: T | null | undefined): value is T {
return value != null;
}
export function LatencyChart({ height }: Props) {
const history = useHistory();
const theme = useTheme();
const comparisonChartTheme = getComparisonChartTheme(theme);
const { urlParams } = useUrlParams();
const { latencyAggregationType } = urlParams;
const { latencyAggregationType, comparisonEnabled } = urlParams;
const license = useLicenseContext();
const {
@ -43,9 +51,19 @@ export function LatencyChart({ height }: Props) {
latencyChartsStatus,
} = useTransactionLatencyChartsFetcher();
const { latencyTimeseries, anomalyTimeseries, mlJobId } = latencyChartsData;
const {
currentPeriod,
previousPeriod,
anomalyTimeseries,
mlJobId,
} = latencyChartsData;
const latencyMaxY = getMaxY(latencyTimeseries);
const timeseries = [
currentPeriod,
comparisonEnabled ? previousPeriod : undefined,
].filter(filterNil);
const latencyMaxY = getMaxY(timeseries);
const latencyFormatter = getDurationFormatter(latencyMaxY);
return (
@ -99,7 +117,8 @@ export function LatencyChart({ height }: Props) {
height={height}
fetchStatus={latencyChartsStatus}
id="latencyChart"
timeseries={latencyTimeseries}
customTheme={comparisonChartTheme}
timeseries={timeseries}
yLabelFormat={getResponseTimeTickFormatter(latencyFormatter)}
anomalyTimeseries={anomalyTimeseries}
/>

View file

@ -84,12 +84,14 @@ function getSelectOptions({
}),
};
const dateDiff = getDateDifference({
start: momentStart,
end: momentEnd,
unitOfTime: 'days',
precise: true,
});
const dateDiff = Number(
getDateDifference({
start: momentStart,
end: momentEnd,
unitOfTime: 'days',
precise: true,
}).toFixed(2)
);
const isRangeToNow = rangeTo === 'now';

View file

@ -12,6 +12,7 @@ import { useUrlParams } from '../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../context/apm_service/use_apm_service_context';
import { getLatencyChartSelector } from '../selectors/latency_chart_selectors';
import { useTheme } from './use_theme';
import { getTimeRangeComparison } from '../components/shared/time_comparison/get_time_range_comparison';
export function useTransactionLatencyChartsFetcher() {
const { serviceName } = useParams<{ serviceName?: string }>();
@ -25,9 +26,16 @@ export function useTransactionLatencyChartsFetcher() {
end,
transactionName,
latencyAggregationType,
comparisonType,
},
} = useUrlParams();
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
start,
end,
comparisonType,
});
const { data, error, status } = useFetcher(
(callApmApi) => {
if (
@ -50,6 +58,8 @@ export function useTransactionLatencyChartsFetcher() {
transactionType,
transactionName,
latencyAggregationType,
comparisonStart,
comparisonEnd,
},
},
});
@ -64,6 +74,8 @@ export function useTransactionLatencyChartsFetcher() {
transactionName,
transactionType,
latencyAggregationType,
comparisonStart,
comparisonEnd,
]
);

View file

@ -18,12 +18,19 @@ const theme = {
euiColorVis5: 'red',
euiColorVis7: 'black',
euiColorVis9: 'yellow',
euiColorLightestShade: 'green',
},
} as EuiTheme;
const latencyChartData = {
overallAvgDuration: 1,
latencyTimeseries: [{ x: 1, y: 10 }],
currentPeriod: {
overallAvgDuration: 1,
latencyTimeseries: [{ x: 1, y: 10 }],
},
previousPeriod: {
overallAvgDuration: 1,
latencyTimeseries: [{ x: 1, y: 10 }],
},
anomalyTimeseries: {
jobId: '1',
anomalyBoundaries: [{ x: 1, y: 2, y0: 1 }],
@ -36,69 +43,84 @@ describe('getLatencyChartSelector', () => {
it('returns default values when data is undefined', () => {
const latencyChart = getLatencyChartSelector({ theme });
expect(latencyChart).toEqual({
latencyTimeseries: [],
currentPeriod: undefined,
previousPeriod: undefined,
mlJobId: undefined,
anomalyTimeseries: undefined,
});
});
it('returns average timeseries', () => {
const { anomalyTimeseries, ...latencyWithouAnomaly } = latencyChartData;
const { anomalyTimeseries, ...latencyWithoutAnomaly } = latencyChartData;
const latencyTimeseries = getLatencyChartSelector({
latencyChart: latencyWithouAnomaly as LatencyChartsResponse,
latencyChart: latencyWithoutAnomaly as LatencyChartsResponse,
theme,
latencyAggregationType: LatencyAggregationType.avg,
});
expect(latencyTimeseries).toEqual({
latencyTimeseries: [
{
title: 'Average',
data: [{ x: 1, y: 10 }],
legendValue: '1 μs',
type: 'linemark',
color: 'blue',
},
],
currentPeriod: {
title: 'Average',
data: [{ x: 1, y: 10 }],
legendValue: '1 μs',
type: 'linemark',
color: 'blue',
},
previousPeriod: {
color: 'green',
data: [{ x: 1, y: 10 }],
type: 'area',
title: 'Previous period',
},
});
});
it('returns 95th percentile timeseries', () => {
const { anomalyTimeseries, ...latencyWithouAnomaly } = latencyChartData;
const { anomalyTimeseries, ...latencyWithoutAnomaly } = latencyChartData;
const latencyTimeseries = getLatencyChartSelector({
latencyChart: latencyWithouAnomaly as LatencyChartsResponse,
latencyChart: latencyWithoutAnomaly as LatencyChartsResponse,
theme,
latencyAggregationType: LatencyAggregationType.p95,
});
expect(latencyTimeseries).toEqual({
latencyTimeseries: [
{
title: '95th percentile',
data: [{ x: 1, y: 10 }],
titleShort: '95th',
type: 'linemark',
color: 'red',
},
],
currentPeriod: {
title: '95th percentile',
titleShort: '95th',
data: [{ x: 1, y: 10 }],
type: 'linemark',
color: 'red',
},
previousPeriod: {
data: [{ x: 1, y: 10 }],
type: 'area',
color: 'green',
title: 'Previous period',
},
});
});
it('returns 99th percentile timeseries', () => {
const { anomalyTimeseries, ...latencyWithouAnomaly } = latencyChartData;
const { anomalyTimeseries, ...latencyWithoutAnomaly } = latencyChartData;
const latencyTimeseries = getLatencyChartSelector({
latencyChart: latencyWithouAnomaly as LatencyChartsResponse,
latencyChart: latencyWithoutAnomaly as LatencyChartsResponse,
theme,
latencyAggregationType: LatencyAggregationType.p99,
});
expect(latencyTimeseries).toEqual({
latencyTimeseries: [
{
title: '99th percentile',
data: [{ x: 1, y: 10 }],
titleShort: '99th',
type: 'linemark',
color: 'black',
},
],
currentPeriod: {
title: '99th percentile',
titleShort: '99th',
data: [{ x: 1, y: 10 }],
type: 'linemark',
color: 'black',
},
previousPeriod: {
data: [{ x: 1, y: 10 }],
type: 'area',
color: 'green',
title: 'Previous period',
},
});
});
});
@ -111,76 +133,52 @@ describe('getLatencyChartSelector', () => {
latencyAggregationType: LatencyAggregationType.p99,
});
expect(latencyTimeseries).toEqual({
currentPeriod: {
title: '99th percentile',
titleShort: '99th',
data: [{ x: 1, y: 10 }],
type: 'linemark',
color: 'black',
},
previousPeriod: {
data: [{ x: 1, y: 10 }],
type: 'area',
color: 'green',
title: 'Previous period',
},
mlJobId: '1',
anomalyTimeseries: {
boundaries: [
{
color: 'rgba(0,0,0,0)',
areaSeriesStyle: {
point: {
opacity: 0,
},
},
data: [
{
x: 1,
y: 1,
},
],
type: 'area',
fit: 'lookahead',
hideLegend: true,
hideTooltipValue: true,
stackAccessors: ['y'],
areaSeriesStyle: { point: { opacity: 0 } },
title: 'anomalyBoundariesLower',
type: 'area',
data: [{ x: 1, y: 1 }],
color: 'rgba(0,0,0,0)',
},
{
color: 'rgba(0,0,255,0.5)',
areaSeriesStyle: {
point: {
opacity: 0,
},
},
data: [
{
x: 1,
y: 1,
},
],
type: 'area',
fit: 'lookahead',
hideLegend: true,
hideTooltipValue: true,
stackAccessors: ['y'],
areaSeriesStyle: { point: { opacity: 0 } },
title: 'anomalyBoundariesUpper',
type: 'area',
data: [{ x: 1, y: 1 }],
color: 'rgba(0,0,255,0.5)',
},
],
scores: {
color: 'yellow',
data: [
{
x: 1,
x0: 2,
},
],
title: 'anomalyScores',
type: 'rectAnnotation',
data: [{ x: 1, x0: 2 }],
color: 'yellow',
},
},
latencyTimeseries: [
{
color: 'black',
data: [
{
x: 1,
y: 10,
},
],
title: '99th percentile',
titleShort: '99th',
type: 'linemark',
},
],
mlJobId: '1',
});
});
});

View file

@ -16,7 +16,8 @@ import { APIReturnType } from '../services/rest/createCallApmApi';
export type LatencyChartsResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/latency'>;
export interface LatencyChartData {
latencyTimeseries: Array<APMChartSpec<Coordinate>>;
currentPeriod?: APMChartSpec<Coordinate>;
previousPeriod?: APMChartSpec<Coordinate>;
mlJobId?: string;
anomalyTimeseries?: { boundaries: APMChartSpec[]; scores: APMChartSpec };
}
@ -29,20 +30,23 @@ export function getLatencyChartSelector({
latencyChart?: LatencyChartsResponse;
theme: EuiTheme;
latencyAggregationType?: string;
}): LatencyChartData {
if (!latencyChart?.latencyTimeseries || !latencyAggregationType) {
return {
latencyTimeseries: [],
mlJobId: undefined,
anomalyTimeseries: undefined,
};
}): Partial<LatencyChartData> {
if (
!latencyChart?.currentPeriod.latencyTimeseries ||
!latencyAggregationType
) {
return {};
}
return {
latencyTimeseries: getLatencyTimeseries({
latencyChart,
currentPeriod: getLatencyTimeseries({
latencyChart: latencyChart.currentPeriod,
theme,
latencyAggregationType,
}),
previousPeriod: getPreviousPeriodTimeseries({
previousPeriod: latencyChart.previousPeriod,
theme,
}),
mlJobId: latencyChart.anomalyTimeseries?.jobId,
anomalyTimeseries: getAnomalyTimeseries({
anomalyTimeseries: latencyChart.anomalyTimeseries,
@ -51,12 +55,30 @@ export function getLatencyChartSelector({
};
}
function getPreviousPeriodTimeseries({
previousPeriod,
theme,
}: {
previousPeriod: LatencyChartsResponse['previousPeriod'];
theme: EuiTheme;
}) {
return {
data: previousPeriod.latencyTimeseries ?? [],
type: 'area',
color: theme.eui.euiColorLightestShade,
title: i18n.translate(
'xpack.apm.serviceOverview.latencyChartTitle.previousPeriodLabel',
{ defaultMessage: 'Previous period' }
),
};
}
function getLatencyTimeseries({
latencyChart,
theme,
latencyAggregationType,
}: {
latencyChart: LatencyChartsResponse;
latencyChart: LatencyChartsResponse['currentPeriod'];
theme: EuiTheme;
latencyAggregationType: string;
}) {
@ -65,49 +87,42 @@ function getLatencyTimeseries({
switch (latencyAggregationType) {
case 'avg': {
return [
{
title: i18n.translate(
'xpack.apm.transactions.latency.chart.averageLabel',
{ defaultMessage: 'Average' }
),
data: latencyTimeseries,
legendValue: asDuration(overallAvgDuration),
type: 'linemark',
color: theme.eui.euiColorVis1,
},
];
return {
title: i18n.translate(
'xpack.apm.transactions.latency.chart.averageLabel',
{ defaultMessage: 'Average' }
),
data: latencyTimeseries,
legendValue: asDuration(overallAvgDuration),
type: 'linemark',
color: theme.eui.euiColorVis1,
};
}
case 'p95': {
return [
{
title: i18n.translate(
'xpack.apm.transactions.latency.chart.95thPercentileLabel',
{ defaultMessage: '95th percentile' }
),
titleShort: '95th',
data: latencyTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis5,
},
];
return {
title: i18n.translate(
'xpack.apm.transactions.latency.chart.95thPercentileLabel',
{ defaultMessage: '95th percentile' }
),
titleShort: '95th',
data: latencyTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis5,
};
}
case 'p99': {
return [
{
title: i18n.translate(
'xpack.apm.transactions.latency.chart.99thPercentileLabel',
{ defaultMessage: '99th percentile' }
),
titleShort: '99th',
data: latencyTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis7,
},
];
return {
title: i18n.translate(
'xpack.apm.transactions.latency.chart.99thPercentileLabel',
{ defaultMessage: '99th percentile' }
),
titleShort: '99th',
data: latencyTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis7,
};
}
}
return [];
}
function getAnomalyTimeseries({

View file

@ -47,7 +47,6 @@ export async function getServiceTransactionGroupComparisonStatistics({
latencyAggregationType,
start,
end,
getOffsetXCoordinate,
}: {
environment?: string;
kuery?: string;
@ -60,7 +59,6 @@ export async function getServiceTransactionGroupComparisonStatistics({
latencyAggregationType: LatencyAggregationType;
start: number;
end: number;
getOffsetXCoordinate?: (timeseries: Coordinate[]) => Coordinate[];
}): Promise<
Array<{
transactionName: string;
@ -175,15 +173,9 @@ export async function getServiceTransactionGroupComparisonStatistics({
bucket.transaction_group_total_duration.value || 0;
return {
transactionName,
latency: getOffsetXCoordinate
? getOffsetXCoordinate(latency)
: latency,
throughput: getOffsetXCoordinate
? getOffsetXCoordinate(throughput)
: throughput,
errorRate: getOffsetXCoordinate
? getOffsetXCoordinate(errorRate)
: errorRate,
latency,
throughput,
errorRate,
impact: totalDuration
? (transactionGroupTotalDuration * 100) / totalDuration
: 0,
@ -244,12 +236,6 @@ export async function getServiceTransactionGroupComparisonStatisticsPeriods({
...commonProps,
start: comparisonStart,
end: comparisonEnd,
getOffsetXCoordinate: (timeseries: Coordinate[]) =>
offsetPreviousPeriodCoordinates({
currentPeriodStart: start,
previousPeriodStart: comparisonStart,
previousPeriodTimeseries: timeseries,
}),
})
: [];
@ -258,8 +244,29 @@ export async function getServiceTransactionGroupComparisonStatisticsPeriods({
previousPeriodPromise,
]);
const firtCurrentPeriod = currentPeriod.length ? currentPeriod[0] : undefined;
return {
currentPeriod: keyBy(currentPeriod, 'transactionName'),
previousPeriod: keyBy(previousPeriod, 'transactionName'),
previousPeriod: keyBy(
previousPeriod.map((data) => {
return {
...data,
errorRate: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: firtCurrentPeriod?.errorRate,
previousPeriodTimeseries: data.errorRate,
}),
throughput: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: firtCurrentPeriod?.throughput,
previousPeriodTimeseries: data.throughput,
}),
latency: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: firtCurrentPeriod?.latency,
previousPeriodTimeseries: data.latency,
}),
};
}),
'transactionName'
),
};
}

View file

@ -25,6 +25,7 @@ import {
} from '../../../lib/helpers/aggregated_transactions';
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request';
import { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate';
import { withApmSpan } from '../../../utils/with_apm_span';
import {
getLatencyAggregation,
@ -43,17 +44,21 @@ function searchLatency({
setup,
searchAggregatedTransactions,
latencyAggregationType,
start,
end,
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType: string | undefined;
transactionName: string | undefined;
setup: Setup & SetupTimeRange;
setup: Setup;
searchAggregatedTransactions: boolean;
latencyAggregationType: LatencyAggregationType;
start: number;
end: number;
}) {
const { start, end, apmEventClient } = setup;
const { apmEventClient } = setup;
const { intervalString } = getBucketSize({ start, end });
const filter: ESFilter[] = [
@ -119,15 +124,19 @@ export function getLatencyTimeseries({
setup,
searchAggregatedTransactions,
latencyAggregationType,
start,
end,
}: {
environment?: string;
kuery?: string;
serviceName: string;
transactionType: string | undefined;
transactionName: string | undefined;
setup: Setup & SetupTimeRange;
setup: Setup;
searchAggregatedTransactions: boolean;
latencyAggregationType: LatencyAggregationType;
start: number;
end: number;
}) {
return withApmSpan('get_latency_charts', async () => {
const response = await searchLatency({
@ -139,6 +148,8 @@ export function getLatencyTimeseries({
setup,
searchAggregatedTransactions,
latencyAggregationType,
start,
end,
});
if (!response.aggregations) {
@ -162,3 +173,65 @@ export function getLatencyTimeseries({
};
});
}
export async function getLatencyPeriods({
serviceName,
transactionType,
transactionName,
setup,
searchAggregatedTransactions,
latencyAggregationType,
comparisonStart,
comparisonEnd,
}: {
serviceName: string;
transactionType: string | undefined;
transactionName: string | undefined;
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
latencyAggregationType: LatencyAggregationType;
comparisonStart?: number;
comparisonEnd?: number;
}) {
const { start, end } = setup;
const options = {
serviceName,
transactionType,
transactionName,
setup,
searchAggregatedTransactions,
};
const currentPeriodPromise = getLatencyTimeseries({
...options,
start,
end,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
});
const previousPeriodPromise =
comparisonStart && comparisonEnd
? getLatencyTimeseries({
...options,
start: comparisonStart,
end: comparisonEnd,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
})
: { latencyTimeseries: [], overallAvgDuration: null };
const [currentPeriod, previousPeriod] = await Promise.all([
currentPeriodPromise,
previousPeriodPromise,
]);
return {
currentPeriod,
previousPeriod: {
...previousPeriod,
latencyTimeseries: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: currentPeriod.latencyTimeseries,
previousPeriodTimeseries: previousPeriod.latencyTimeseries,
}),
},
};
}

View file

@ -408,19 +408,16 @@ export const serviceThroughputRoute = createRoute({
...commonProps,
start: comparisonStart,
end: comparisonEnd,
}).then((coordinates) =>
offsetPreviousPeriodCoordinates({
currentPeriodStart: start,
previousPeriodStart: comparisonStart,
previousPeriodTimeseries: coordinates,
})
)
})
: [],
]);
return {
currentPeriod,
previousPeriod,
previousPeriod: offsetPreviousPeriodCoordinates({
currentPeriodTimeseries: currentPeriod,
previousPeriodTimeseries: previousPeriod,
}),
};
},
});

View file

@ -19,14 +19,14 @@ import { getServiceTransactionGroupComparisonStatisticsPeriods } from '../lib/se
import { getTransactionBreakdown } from '../lib/transactions/breakdown';
import { getTransactionDistribution } from '../lib/transactions/distribution';
import { getAnomalySeries } from '../lib/transactions/get_anomaly_data';
import { getLatencyTimeseries } from '../lib/transactions/get_latency_charts';
import { getLatencyPeriods } from '../lib/transactions/get_latency_charts';
import { getThroughputCharts } from '../lib/transactions/get_throughput_charts';
import { getTransactionGroupList } from '../lib/transaction_groups';
import { getErrorRate } from '../lib/transaction_groups/get_error_rate';
import { createRoute } from './create_route';
import {
environmentRt,
comparisonRangeRt,
environmentRt,
rangeRt,
kueryRt,
} from './default_api_types';
@ -179,16 +179,12 @@ export const transactionLatencyChartsRoute = createRoute({
serviceName: t.string,
}),
query: t.intersection([
t.partial({
transactionName: t.string,
}),
t.type({
transactionType: t.string,
latencyAggregationType: latencyAggregationTypeRt,
}),
environmentRt,
kueryRt,
rangeRt,
t.partial({ transactionName: t.string }),
t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]),
]),
}),
options: { tags: ['access:apm'] },
@ -202,6 +198,8 @@ export const transactionLatencyChartsRoute = createRoute({
transactionType,
transactionName,
latencyAggregationType,
comparisonStart,
comparisonEnd,
} = context.params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
@ -219,10 +217,15 @@ export const transactionLatencyChartsRoute = createRoute({
logger,
};
const [latencyData, anomalyTimeseries] = await Promise.all([
getLatencyTimeseries({
const [
{ currentPeriod, previousPeriod },
anomalyTimeseries,
] = await Promise.all([
getLatencyPeriods({
...options,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
comparisonStart,
comparisonEnd,
}),
getAnomalySeries(options).catch((error) => {
logger.warn(`Unable to retrieve anomalies for latency charts.`);
@ -231,11 +234,9 @@ export const transactionLatencyChartsRoute = createRoute({
}),
]);
const { latencyTimeseries, overallAvgDuration } = latencyData;
return {
latencyTimeseries,
overallAvgDuration,
currentPeriod,
previousPeriod,
anomalyTimeseries,
};
},

View file

@ -7,16 +7,16 @@
import { Coordinate } from '../../typings/timeseries';
import { offsetPreviousPeriodCoordinates } from './offset_previous_period_coordinate';
const previousPeriodStart = new Date('2021-01-27T14:45:00.000Z').valueOf();
const currentPeriodStart = new Date('2021-01-28T14:45:00.000Z').valueOf();
const currentPeriodTimeseries: Coordinate[] = [
{ x: new Date('2021-01-28T14:45:00.000Z').valueOf(), y: 0 },
];
describe('mergePeriodsTimeseries', () => {
describe('returns empty array', () => {
it('when previous timeseries is not defined', () => {
expect(
offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
currentPeriodTimeseries,
previousPeriodTimeseries: undefined,
})
).toEqual([]);
@ -25,8 +25,7 @@ describe('mergePeriodsTimeseries', () => {
it('when previous timeseries is empty', () => {
expect(
offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
currentPeriodTimeseries,
previousPeriodTimeseries: [],
})
).toEqual([]);
@ -43,8 +42,7 @@ describe('mergePeriodsTimeseries', () => {
expect(
offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
currentPeriodTimeseries,
previousPeriodTimeseries,
})
).toEqual([

View file

@ -9,19 +9,20 @@ import moment from 'moment';
import { Coordinate } from '../../typings/timeseries';
export function offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
currentPeriodTimeseries,
previousPeriodTimeseries,
}: {
currentPeriodStart: number;
previousPeriodStart: number;
currentPeriodTimeseries?: Coordinate[];
previousPeriodTimeseries?: Coordinate[];
}) {
if (!previousPeriodTimeseries) {
if (!previousPeriodTimeseries?.length) {
return [];
}
const currentPeriodStart = currentPeriodTimeseries?.length
? currentPeriodTimeseries[0].x
: 0;
const dateDiff = currentPeriodStart - previousPeriodStart;
const dateDiff = currentPeriodStart - previousPeriodTimeseries[0].x;
return previousPeriodTimeseries.map(({ x, y }) => {
const offsetX = moment(x).add(dateDiff).valueOf();

View file

@ -1,31 +1,182 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`APM API tests trial apm_8.0.0 Transaction latency with a trial license when data is loaded when not defined environment is selected should return the correct anomaly boundaries 1`] = `
exports[`APM API tests basic apm_8.0.0 Latency with a basic license when data is loaded time comparison returns some data 1`] = `
Array [
Object {
"x": 1607436000000,
"y": 0,
"y0": 0,
"x": 1607436780000,
"y": 51029,
},
Object {
"x": 1607436900000,
"y": 0,
"y0": 0,
"x": 1607436820000,
"y": 38124,
},
Object {
"x": 1607436860000,
"y": 16327,
},
Object {
"x": 1607436980000,
"y": 35617.5,
},
Object {
"x": 1607436990000,
"y": 34599.75,
},
Object {
"x": 1607437100000,
"y": 26980,
},
Object {
"x": 1607437110000,
"y": 42808,
},
Object {
"x": 1607437130000,
"y": 22230.5,
},
Object {
"x": 1607437220000,
"y": 34973,
},
Object {
"x": 1607437230000,
"y": 19284.2,
},
Object {
"x": 1607437240000,
"y": 9280,
},
Object {
"x": 1607437250000,
"y": 42777,
},
Object {
"x": 1607437260000,
"y": 10702,
},
Object {
"x": 1607437340000,
"y": 22452,
},
Object {
"x": 1607437470000,
"y": 14495.5,
},
Object {
"x": 1607437480000,
"y": 11644.5714285714,
},
Object {
"x": 1607437570000,
"y": 17359.6666666667,
},
Object {
"x": 1607437590000,
"y": 11394.2,
},
]
`;
exports[`APM API tests trial apm_8.0.0 Transaction latency with a trial license when data is loaded with environment selected and empty kuery filter should return a non-empty anomaly series 1`] = `
exports[`APM API tests basic apm_8.0.0 Latency with a basic license when data is loaded time comparison returns some data 2`] = `
Array [
Object {
"x": 1607436800000,
"y": 23448.25,
},
Object {
"x": 1607436820000,
"y": 25181,
},
Object {
"x": 1607436840000,
"y": 16834,
},
Object {
"x": 1607436910000,
"y": 21582,
},
Object {
"x": 1607437040000,
"y": 31800,
},
Object {
"x": 1607437050000,
"y": 21341,
},
Object {
"x": 1607437060000,
"y": 21108.5,
},
Object {
"x": 1607437150000,
"y": 12147.3333333333,
},
Object {
"x": 1607437160000,
"y": 23941.5,
},
Object {
"x": 1607437180000,
"y": 18244,
},
Object {
"x": 1607437240000,
"y": 24359.5,
},
Object {
"x": 1607437280000,
"y": 27767,
},
Object {
"x": 1607437290000,
"y": 21909.6666666667,
},
Object {
"x": 1607437390000,
"y": 31521,
},
Object {
"x": 1607437410000,
"y": 20227.5,
},
Object {
"x": 1607437420000,
"y": 18664,
},
Object {
"x": 1607437510000,
"y": 14197.5,
},
Object {
"x": 1607437520000,
"y": 19199.8571428571,
},
Object {
"x": 1607437540000,
"y": 63745.75,
},
Object {
"x": 1607437640000,
"y": 63220,
},
Object {
"x": 1607437660000,
"y": 20040,
},
]
`;
exports[`APM API tests trial apm_8.0.0 Transaction latency with a trial license when data is loaded when not defined environments is seleted should return the correct anomaly boundaries 1`] = `
Array [
Object {
"x": 1607436000000,
"y": 1625128.56211579,
"y0": 7533.02707532227,
"y": 0,
"y0": 0,
},
Object {
"x": 1607436900000,
"y": 1660982.24115757,
"y0": 5732.00699123528,
"y": 0,
"y0": 0,
},
]
`;
@ -34,13 +185,13 @@ exports[`APM API tests trial apm_8.0.0 Transaction latency with a trial license
Array [
Object {
"x": 1607436000000,
"y": 1625128.56211579,
"y0": 7533.02707532227,
"y": 136610.507897203,
"y0": 22581.5157631454,
},
Object {
"x": 1607436900000,
"y": 1660982.24115757,
"y0": 5732.00699123528,
"y": 136610.507897203,
"y0": 22581.5157631454,
},
]
`;

View file

@ -6,21 +6,22 @@
*/
import expect from '@kbn/expect';
import url from 'url';
import moment from 'moment';
import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi';
import { PromiseReturnType } from '../../../../plugins/observability/typings/common';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
import { registry } from '../../common/registry';
type LatencyChartReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/latency'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const archiveName = 'apm_8.0.0';
const range = archives_metadata[archiveName];
// url parameters
const start = encodeURIComponent(range.start);
const end = encodeURIComponent(range.end);
const { start, end } = archives_metadata[archiveName];
registry.when(
'Latency with a basic license when data is not loaded ',
@ -28,7 +29,15 @@ export default function ApiTest({ getService }: FtrProviderContext) {
() => {
it('returns 400 when latencyAggregationType is not informed', async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=request`
url.format({
pathname: `/api/apm/services/opbeans-node/transactions/charts/latency`,
query: {
start,
end,
transactionType: 'request',
environment: 'testing',
},
})
);
expect(response.status).to.be(400);
@ -36,7 +45,15 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('returns 400 when transactionType is not informed', async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?environment=testing&start=${start}&end=${end}&latencyAggregationType=avg`
url.format({
pathname: `/api/apm/services/opbeans-node/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'avg',
environment: 'testing',
},
})
);
expect(response.status).to.be(400);
@ -44,13 +61,25 @@ export default function ApiTest({ getService }: FtrProviderContext) {
it('handles the empty state', async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?environment=testing&start=${start}&end=${end}&latencyAggregationType=avg&transactionType=request`
url.format({
pathname: `/api/apm/services/opbeans-node/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'avg',
transactionType: 'request',
environment: 'testing',
},
})
);
expect(response.status).to.be(200);
expect(response.body.overallAvgDuration).to.be(null);
expect(response.body.latencyTimeseries.length).to.be(0);
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.overallAvgDuration).to.be(null);
expect(latencyChartReturn.currentPeriod.latencyTimeseries.length).to.be(0);
expect(latencyChartReturn.previousPeriod.latencyTimeseries.length).to.be(0);
});
}
);
@ -64,42 +93,113 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('average latency type', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=request&latencyAggregationType=avg`
url.format({
pathname: `/api/apm/services/opbeans-node/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'avg',
transactionType: 'request',
environment: 'testing',
},
})
);
});
it('returns average duration and timeseries', async () => {
expect(response.status).to.be(200);
expect(response.body.overallAvgDuration).not.to.be(null);
expect(response.body.latencyTimeseries.length).to.be.eql(61);
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.overallAvgDuration).not.to.be(null);
expect(latencyChartReturn.currentPeriod.latencyTimeseries.length).to.be.eql(61);
});
});
describe('95th percentile latency type', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=request&latencyAggregationType=p95`
url.format({
pathname: `/api/apm/services/opbeans-node/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'p95',
transactionType: 'request',
environment: 'testing',
},
})
);
});
it('returns average duration and timeseries', async () => {
expect(response.status).to.be(200);
expect(response.body.overallAvgDuration).not.to.be(null);
expect(response.body.latencyTimeseries.length).to.be.eql(61);
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.overallAvgDuration).not.to.be(null);
expect(latencyChartReturn.currentPeriod.latencyTimeseries.length).to.be.eql(61);
});
});
describe('99th percentile latency type', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=request&latencyAggregationType=p99`
url.format({
pathname: `/api/apm/services/opbeans-node/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'p99',
transactionType: 'request',
environment: 'testing',
},
})
);
});
it('returns average duration and timeseries', async () => {
expect(response.status).to.be(200);
expect(response.body.overallAvgDuration).not.to.be(null);
expect(response.body.latencyTimeseries.length).to.be.eql(61);
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.overallAvgDuration).not.to.be(null);
expect(latencyChartReturn.currentPeriod.latencyTimeseries.length).to.be.eql(61);
});
});
describe('time comparison', () => {
before(async () => {
response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-node/transactions/charts/latency`,
query: {
latencyAggregationType: 'avg',
transactionType: 'request',
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
comparisonStart: start,
comparisonEnd: moment(start).add(15, 'minutes').toISOString(),
},
})
);
});
it('returns some data', async () => {
expect(response.status).to.be(200);
const latencyChartReturn = response.body as LatencyChartReturnType;
const currentPeriodNonNullDataPoints = latencyChartReturn.currentPeriod.latencyTimeseries.filter(
({ y }) => y !== null
);
expect(currentPeriodNonNullDataPoints.length).to.be.greaterThan(0);
const previousPeriodNonNullDataPoints = latencyChartReturn.previousPeriod.latencyTimeseries.filter(
({ y }) => y !== null
);
expect(previousPeriodNonNullDataPoints.length).to.be.greaterThan(0);
expectSnapshot(currentPeriodNonNullDataPoints).toMatch();
expectSnapshot(previousPeriodNonNullDataPoints).toMatch();
});
it('matches x-axis on current period and previous period', () => {
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn.currentPeriod.latencyTimeseries.map(({ x }) => x)).to.be.eql(
latencyChartReturn.previousPeriod.latencyTimeseries.map(({ x }) => x)
);
});
});
}
@ -116,7 +216,15 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('without an environment', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg`
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'avg',
transactionType,
},
})
);
});
@ -128,7 +236,16 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('with environment selected', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?environment=production&start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg`
url.format({
pathname: `/api/apm/services/opbeans-python/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'avg',
transactionType,
environment: 'production',
},
})
);
});
@ -137,24 +254,37 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('should return the ML job id for anomalies of the selected environment', () => {
expect(response.body).to.have.property('anomalyTimeseries');
expect(response.body.anomalyTimeseries).to.have.property('jobId');
expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline(
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn).to.have.property('anomalyTimeseries');
expect(latencyChartReturn.anomalyTimeseries).to.have.property('jobId');
expectSnapshot(latencyChartReturn.anomalyTimeseries?.jobId).toMatchInline(
`"apm-production-1369-high_mean_transaction_duration"`
);
});
it('should return a non-empty anomaly series', () => {
expect(response.body).to.have.property('anomalyTimeseries');
expect(response.body.anomalyTimeseries.anomalyBoundaries?.length).to.be.greaterThan(0);
expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch();
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn).to.have.property('anomalyTimeseries');
expect(latencyChartReturn.anomalyTimeseries?.anomalyBoundaries?.length).to.be.greaterThan(
0
);
expectSnapshot(latencyChartReturn.anomalyTimeseries?.anomalyBoundaries).toMatch();
});
});
describe('when not defined environment is selected', () => {
describe('when not defined environments is seleted', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-python/transactions/charts/latency?environment=ENVIRONMENT_NOT_DEFINED&start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg`
url.format({
pathname: `/api/apm/services/opbeans-python/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'avg',
transactionType,
environment: 'ENVIRONMENT_NOT_DEFINED',
},
})
);
});
@ -163,23 +293,34 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('should return the ML job id for anomalies with no defined environment', () => {
expect(response.body).to.have.property('anomalyTimeseries');
expect(response.body.anomalyTimeseries).to.have.property('jobId');
expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline(
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn).to.have.property('anomalyTimeseries');
expect(latencyChartReturn.anomalyTimeseries).to.have.property('jobId');
expectSnapshot(latencyChartReturn.anomalyTimeseries?.jobId).toMatchInline(
`"apm-environment_not_defined-5626-high_mean_transaction_duration"`
);
});
it('should return the correct anomaly boundaries', () => {
expect(response.body).to.have.property('anomalyTimeseries');
expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch();
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn).to.have.property('anomalyTimeseries');
expectSnapshot(latencyChartReturn.anomalyTimeseries?.anomalyBoundaries).toMatch();
});
});
describe('with all environments selected', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?environment=ENVIRONMENT_ALL&start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg`
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/charts/latency`,
query: {
start,
end,
latencyAggregationType: 'avg',
transactionType,
environment: 'ENVIRONMENT_ALL',
},
})
);
});
@ -188,33 +329,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('should not return anomaly timeseries data', () => {
expect(response.body).to.not.have.property('anomalyTimeseries');
});
});
describe('with environment selected and empty kuery filter', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?environment=production&start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg`
);
});
it('should have a successful response', () => {
expect(response.status).to.eql(200);
});
it('should return the ML job id for anomalies of the selected environment', () => {
expect(response.body).to.have.property('anomalyTimeseries');
expect(response.body.anomalyTimeseries).to.have.property('jobId');
expectSnapshot(response.body.anomalyTimeseries.jobId).toMatchInline(
`"apm-production-1369-high_mean_transaction_duration"`
);
});
it('should return a non-empty anomaly series', () => {
expect(response.body).to.have.property('anomalyTimeseries');
expect(response.body.anomalyTimeseries.anomalyBoundaries?.length).to.be.greaterThan(0);
expectSnapshot(response.body.anomalyTimeseries.anomalyBoundaries).toMatch();
const latencyChartReturn = response.body as LatencyChartReturnType;
expect(latencyChartReturn).to.not.have.property('anomalyTimeseries');
});
});
}

View file

@ -77,8 +77,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(Object.keys(currentPeriod).sort()).to.be.eql(transactionNames.sort());
const currentPeriodItems = Object.values(currentPeriod).map((data) => data);
const previousPeriodItems = Object.values(previousPeriod).map((data) => data);
const currentPeriodItems = Object.values(currentPeriod);
const previousPeriodItems = Object.values(previousPeriod);
expect(previousPeriodItems.length).to.be.eql(0);
@ -210,8 +210,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('returns correct latency data', () => {
const currentPeriodItems = Object.values(currentPeriod).map((data) => data);
const previousPeriodItems = Object.values(previousPeriod).map((data) => data);
const currentPeriodItems = Object.values(currentPeriod);
const previousPeriodItems = Object.values(previousPeriod);
const currentPeriodFirstItem = currentPeriodItems[0];
const previousPeriodFirstItem = previousPeriodItems[0];
@ -227,8 +227,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('returns correct throughput data', () => {
const currentPeriodItems = Object.values(currentPeriod).map((data) => data);
const previousPeriodItems = Object.values(previousPeriod).map((data) => data);
const currentPeriodItems = Object.values(currentPeriod);
const previousPeriodItems = Object.values(previousPeriod);
const currentPeriodFirstItem = currentPeriodItems[0];
const previousPeriodFirstItem = previousPeriodItems[0];
@ -244,8 +244,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('returns correct error rate data', () => {
const currentPeriodItems = Object.values(currentPeriod).map((data) => data);
const previousPeriodItems = Object.values(previousPeriod).map((data) => data);
const currentPeriodItems = Object.values(currentPeriod);
const previousPeriodItems = Object.values(previousPeriod);
const currentPeriodFirstItem = currentPeriodItems[0];
const previousPeriodFirstItem = previousPeriodItems[0];
@ -256,13 +256,26 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(
removeEmptyCoordinates(previousPeriodFirstItem.errorRate).length
).to.be.greaterThan(0);
expectSnapshot(currentPeriodFirstItem.errorRate).toMatch();
expectSnapshot(previousPeriodFirstItem.errorRate).toMatch();
});
it('matches x-axis on current period and previous period', () => {
const currentPeriodItems = Object.values(currentPeriod);
const previousPeriodItems = Object.values(previousPeriod);
const currentPeriodFirstItem = currentPeriodItems[0];
const previousPeriodFirstItem = previousPeriodItems[0];
expect(currentPeriodFirstItem.errorRate.map(({ x }) => x)).to.be.eql(
previousPeriodFirstItem.errorRate.map(({ x }) => x)
);
});
it('returns correct impact data', () => {
const currentPeriodItems = Object.values(currentPeriod).map((data) => data);
const previousPeriodItems = Object.values(previousPeriod).map((data) => data);
const currentPeriodItems = Object.values(currentPeriod);
const previousPeriodItems = Object.values(previousPeriod);
const currentPeriodFirstItem = currentPeriodItems[0];
const previousPeriodFirstItem = previousPeriodItems[0];