[APM] Adding comparison to throughput chart (#90128) (#91353)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Dario Gieselaar <dario.gieselaar@elastic.co>

Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2021-02-15 07:12:29 +01:00 committed by GitHub
parent 656d844e80
commit 15bb592c02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1456 additions and 184 deletions

View file

@ -1,29 +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 { dateAsStringRt } from './index';
import { isLeft, isRight } from 'fp-ts/lib/Either';
describe('dateAsStringRt', () => {
it('validates whether a string is a valid date', () => {
expect(isLeft(dateAsStringRt.decode(1566299881499))).toBe(true);
expect(isRight(dateAsStringRt.decode('2019-08-20T11:18:31.407Z'))).toBe(
true
);
});
it('returns the string it was given', () => {
const either = dateAsStringRt.decode('2019-08-20T11:18:31.407Z');
if (isRight(either)) {
expect(either.right).toBe('2019-08-20T11:18:31.407Z');
} else {
fail();
}
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { isoToEpochRt } from './index';
import { isRight } from 'fp-ts/lib/Either';
describe('isoToEpochRt', () => {
it('validates whether its input is a valid ISO timestamp', () => {
expect(isRight(isoToEpochRt.decode(1566299881499))).toBe(false);
expect(isRight(isoToEpochRt.decode('2019-08-20T11:18:31.407Z'))).toBe(true);
});
it('decodes valid ISO timestamps to epoch time', () => {
const iso = '2019-08-20T11:18:31.407Z';
const result = isoToEpochRt.decode(iso);
if (isRight(result)) {
expect(result.right).toBe(new Date(iso).getTime());
} else {
fail();
}
});
it('encodes epoch time to ISO string', () => {
expect(isoToEpochRt.encode(1566299911407)).toBe('2019-08-20T11:18:31.407Z');
});
});

View file

@ -9,15 +9,20 @@ import * as t from 'io-ts';
import { either } from 'fp-ts/lib/Either';
// Checks whether a string is a valid ISO timestamp,
// but doesn't convert it into a Date object when decoding
// and returns an epoch timestamp
export const dateAsStringRt = new t.Type<string, string, unknown>(
'DateAsString',
t.string.is,
export const isoToEpochRt = new t.Type<number, string, unknown>(
'isoToEpochRt',
t.number.is,
(input, context) =>
either.chain(t.string.validate(input, context), (str) => {
const date = new Date(str);
return isNaN(date.getTime()) ? t.failure(input, context) : t.success(str);
const epochDate = new Date(str).getTime();
return isNaN(epochDate)
? t.failure(input, context)
: t.success(epochDate);
}),
t.identity
(a) => {
const d = new Date(a);
return d.toISOString();
}
);

View file

@ -0,0 +1,24 @@
/*
* 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 * as t from 'io-ts';
export const toBooleanRt = new t.Type<boolean, unknown, unknown>(
'ToBoolean',
t.boolean.is,
(input) => {
let value: boolean;
if (typeof input === 'string') {
value = input === 'true';
} else {
value = !!input;
}
return t.success(value);
},
t.identity
);

View file

@ -9,7 +9,7 @@ import * as t from 'io-ts';
export const toNumberRt = new t.Type<number, unknown, unknown>(
'ToNumber',
t.any.is,
t.number.is,
(input, context) => {
const number = Number(input);
return !isNaN(number) ? t.success(number) : t.failure(input, context);

View file

@ -20,10 +20,12 @@ import {
MockApmPluginContextWrapper,
} from '../../../context/apm_plugin/mock_apm_plugin_context';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { clearCache } from '../../../services/rest/callApi';
import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern';
import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import * as hook from './use_anomaly_detection_jobs_fetcher';
import { TimeRangeComparisonType } from '../../shared/time_comparison/get_time_range_comparison';
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiCounter: () => {} },
@ -55,10 +57,10 @@ function wrapper({ children }: { children?: ReactNode }) {
params={{
rangeFrom: 'now-15m',
rangeTo: 'now',
start: 'mystart',
end: 'myend',
start: '2021-02-12T13:20:43.344Z',
end: '2021-02-12T13:20:58.344Z',
comparisonEnabled: true,
comparisonType: 'yesterday',
comparisonType: TimeRangeComparisonType.DayBefore,
}}
>
{children}
@ -74,6 +76,7 @@ describe('ServiceInventory', () => {
beforeEach(() => {
// @ts-expect-error
global.sessionStorage = new SessionStorageMock();
clearCache();
jest.spyOn(hook, 'useAnomalyDetectionJobsFetcher').mockReturnValue({
anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS,

View file

@ -15,6 +15,15 @@ import { useTheme } from '../../../hooks/use_theme';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
import {
getTimeRangeComparison,
getComparisonChartTheme,
} from '../../shared/time_comparison/get_time_range_comparison';
const INITIAL_STATE = {
currentPeriod: [],
previousPeriod: [],
};
export function ServiceOverviewThroughputChart({
height,
@ -25,9 +34,20 @@ export function ServiceOverviewThroughputChart({
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { transactionType } = useApmServiceContext();
const { start, end } = urlParams;
const { start, end, comparisonEnabled, comparisonType } = urlParams;
const comparisonChartTheme = getComparisonChartTheme(theme);
const {
comparisonStart = undefined,
comparisonEnd = undefined,
} = comparisonType
? getTimeRangeComparison({
start,
end,
comparisonType,
})
: {};
const { data, status } = useFetcher(
const { data = INITIAL_STATE, status } = useFetcher(
(callApmApi) => {
if (serviceName && transactionType && start && end) {
return callApmApi({
@ -41,12 +61,22 @@ export function ServiceOverviewThroughputChart({
end,
transactionType,
uiFilters: JSON.stringify(uiFilters),
comparisonStart,
comparisonEnd,
},
},
});
}
},
[serviceName, start, end, uiFilters, transactionType]
[
serviceName,
start,
end,
uiFilters,
transactionType,
comparisonStart,
comparisonEnd,
]
);
return (
@ -63,9 +93,10 @@ export function ServiceOverviewThroughputChart({
height={height}
showAnnotations={false}
fetchStatus={status}
customTheme={comparisonChartTheme}
timeseries={[
{
data: data?.throughput ?? [],
data: data.currentPeriod,
type: 'linemark',
color: theme.eui.euiColorVis0,
title: i18n.translate(
@ -73,6 +104,21 @@ export function ServiceOverviewThroughputChart({
{ defaultMessage: 'Throughput' }
),
},
...(comparisonEnabled
? [
{
data: data.previousPeriod,
type: 'area',
color: theme.eui.euiColorLightestShade,
title: i18n.translate(
'xpack.apm.serviceOverview.throughtputChart.previousPeriodLabel',
{
defaultMessage: 'Previous period',
}
),
},
]
: []),
]}
yLabelFormat={asTransactionRate}
/>

View file

@ -131,7 +131,7 @@ describe('TransactionOverview', () => {
});
expect(history.location.search).toEqual(
'?transactionType=secondType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=yesterday'
'?transactionType=secondType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=day'
);
expect(getByText(container, 'firstType')).toBeInTheDocument();
expect(getByText(container, 'secondType')).toBeInTheDocument();
@ -142,7 +142,7 @@ describe('TransactionOverview', () => {
expect(history.push).toHaveBeenCalled();
expect(history.location.search).toEqual(
'?transactionType=firstType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=yesterday'
'?transactionType=firstType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=day'
);
});
});

View file

@ -59,6 +59,7 @@ interface Props {
anomalyTimeseries?: ReturnType<
typeof getLatencyChartSelector
>['anomalyTimeseries'];
customTheme?: Record<string, unknown>;
}
export function TimeseriesChart({
@ -72,13 +73,14 @@ export function TimeseriesChart({
showAnnotations = true,
yDomain,
anomalyTimeseries,
customTheme = {},
}: Props) {
const history = useHistory();
const { annotations } = useAnnotationsContext();
const chartTheme = useChartTheme();
const { setPointerEvent, chartRef } = useChartPointerEventContext();
const { urlParams } = useUrlParams();
const theme = useTheme();
const chartTheme = useChartTheme();
const { start, end } = urlParams;
@ -103,6 +105,7 @@ export function TimeseriesChart({
areaSeriesStyle: {
line: { visible: false },
},
...customTheme,
}}
onPointerUpdate={setPointerEvent}
externalPointerEvents={{

View file

@ -0,0 +1,120 @@
/*
* 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 {
getTimeRangeComparison,
TimeRangeComparisonType,
} from './get_time_range_comparison';
describe('getTimeRangeComparison', () => {
describe('return empty object', () => {
it('when start is not defined', () => {
const end = '2021-01-28T15:00:00.000Z';
const result = getTimeRangeComparison({
start: undefined,
end,
comparisonType: TimeRangeComparisonType.DayBefore,
});
expect(result).toEqual({});
});
it('when end is not defined', () => {
const start = '2021-01-28T14:45:00.000Z';
const result = getTimeRangeComparison({
start,
end: undefined,
comparisonType: TimeRangeComparisonType.DayBefore,
});
expect(result).toEqual({});
});
});
describe('Time range is between 0 - 24 hours', () => {
describe('when day before is selected', () => {
it('returns the correct time range - 15 min', () => {
const start = '2021-01-28T14:45:00.000Z';
const end = '2021-01-28T15:00:00.000Z';
const result = getTimeRangeComparison({
comparisonType: TimeRangeComparisonType.DayBefore,
start,
end,
});
expect(result.comparisonStart).toEqual('2021-01-27T14:45:00.000Z');
expect(result.comparisonEnd).toEqual('2021-01-27T15:00:00.000Z');
});
});
describe('when a week before is selected', () => {
it('returns the correct time range - 15 min', () => {
const start = '2021-01-28T14:45:00.000Z';
const end = '2021-01-28T15:00:00.000Z';
const result = getTimeRangeComparison({
comparisonType: TimeRangeComparisonType.WeekBefore,
start,
end,
});
expect(result.comparisonStart).toEqual('2021-01-21T14:45:00.000Z');
expect(result.comparisonEnd).toEqual('2021-01-21T15:00:00.000Z');
});
});
describe('when previous period is selected', () => {
it('returns the correct time range - 15 min', () => {
const start = '2021-02-09T14:40:01.087Z';
const end = '2021-02-09T14:56:00.000Z';
const result = getTimeRangeComparison({
start,
end,
comparisonType: TimeRangeComparisonType.PeriodBefore,
});
expect(result).toEqual({
comparisonStart: '2021-02-09T14:24:02.174Z',
comparisonEnd: '2021-02-09T14:40:01.087Z',
});
});
});
});
describe('Time range is between 24 hours - 1 week', () => {
describe('when a week before is selected', () => {
it('returns the correct time range - 2 days', () => {
const start = '2021-01-26T15:00:00.000Z';
const end = '2021-01-28T15:00:00.000Z';
const result = getTimeRangeComparison({
comparisonType: TimeRangeComparisonType.WeekBefore,
start,
end,
});
expect(result.comparisonStart).toEqual('2021-01-19T15:00:00.000Z');
expect(result.comparisonEnd).toEqual('2021-01-21T15:00:00.000Z');
});
});
});
describe('Time range is greater than 7 days', () => {
it('uses the date difference to calculate the time range - 8 days', () => {
const start = '2021-01-10T15:00:00.000Z';
const end = '2021-01-18T15:00:00.000Z';
const result = getTimeRangeComparison({
comparisonType: TimeRangeComparisonType.PeriodBefore,
start,
end,
});
expect(result.comparisonStart).toEqual('2021-01-02T15:00:00.000Z');
expect(result.comparisonEnd).toEqual('2021-01-10T15:00:00.000Z');
});
it('uses the date difference to calculate the time range - 30 days', () => {
const start = '2021-01-01T15:00:00.000Z';
const end = '2021-01-31T15:00:00.000Z';
const result = getTimeRangeComparison({
comparisonType: TimeRangeComparisonType.PeriodBefore,
start,
end,
});
expect(result.comparisonStart).toEqual('2020-12-02T15:00:00.000Z');
expect(result.comparisonEnd).toEqual('2021-01-01T15:00:00.000Z');
});
});
});

View file

@ -0,0 +1,88 @@
/*
* 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 moment from 'moment';
import { EuiTheme } from 'src/plugins/kibana_react/common';
import { getDateDifference } from '../../../../common/utils/formatters';
export enum TimeRangeComparisonType {
WeekBefore = 'week',
DayBefore = 'day',
PeriodBefore = 'period',
}
export function getComparisonChartTheme(theme: EuiTheme) {
return {
areaSeriesStyle: {
area: {
fill: theme.eui.euiColorLightestShade,
visible: true,
opacity: 1,
},
line: {
stroke: theme.eui.euiColorMediumShade,
strokeWidth: 1,
visible: true,
},
point: {
visible: false,
},
},
};
}
const oneDayInMilliseconds = moment.duration(1, 'day').asMilliseconds();
const oneWeekInMilliseconds = moment.duration(1, 'week').asMilliseconds();
export function getTimeRangeComparison({
comparisonType,
start,
end,
}: {
comparisonType: TimeRangeComparisonType;
start?: string;
end?: string;
}) {
if (!start || !end) {
return {};
}
const startMoment = moment(start);
const endMoment = moment(end);
const startEpoch = startMoment.valueOf();
const endEpoch = endMoment.valueOf();
let diff: number;
switch (comparisonType) {
case TimeRangeComparisonType.DayBefore:
diff = oneDayInMilliseconds;
break;
case TimeRangeComparisonType.WeekBefore:
diff = oneWeekInMilliseconds;
break;
case TimeRangeComparisonType.PeriodBefore:
diff = getDateDifference({
start: startMoment,
end: endMoment,
unitOfTime: 'milliseconds',
precise: true,
});
break;
default:
throw new Error('Unknown comparisonType');
}
return {
comparisonStart: new Date(startEpoch - diff).toISOString(),
comparisonEnd: new Date(endEpoch - diff).toISOString(),
};
}

View file

@ -18,6 +18,7 @@ import {
import { TimeComparison } from './';
import * as urlHelpers from '../../shared/Links/url_helpers';
import moment from 'moment';
import { TimeRangeComparisonType } from './get_time_range_comparison';
function getWrapper(params?: IUrlParams) {
return ({ children }: { children?: ReactNode }) => {
@ -53,22 +54,22 @@ describe('TimeComparison', () => {
expect(spy).toHaveBeenCalledWith(expect.anything(), {
query: {
comparisonEnabled: 'true',
comparisonType: 'yesterday',
comparisonType: TimeRangeComparisonType.DayBefore,
},
});
});
it('selects yesterday and enables comparison', () => {
it('selects day before and enables comparison', () => {
const Wrapper = getWrapper({
start: '2021-01-28T14:45:00.000Z',
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'yesterday',
comparisonType: TimeRangeComparisonType.DayBefore,
rangeTo: 'now',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expectTextsInDocument(component, ['Yesterday', 'A week ago']);
expectTextsInDocument(component, ['Day before', 'Week before']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
@ -80,13 +81,13 @@ describe('TimeComparison', () => {
start: '2021-01-28T10:00:00.000Z',
end: '2021-01-29T10:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'yesterday',
comparisonType: TimeRangeComparisonType.DayBefore,
rangeTo: 'now',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expectTextsInDocument(component, ['Yesterday', 'A week ago']);
expectTextsInDocument(component, ['Day before', 'Week before']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
@ -98,13 +99,13 @@ describe('TimeComparison', () => {
start: '2021-01-28T10:00:00.000Z',
end: '2021-01-29T10:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'previousPeriod',
comparisonType: TimeRangeComparisonType.PeriodBefore,
rangeTo: 'now-15m',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expectTextsInDocument(component, ['28/01 11:00 - 29/01 11:00']);
expectTextsInDocument(component, ['27/01 11:00 - 28/01 11:00']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
@ -118,14 +119,14 @@ describe('TimeComparison', () => {
start: '2021-01-28T10:00:00.000Z',
end: '2021-01-29T11:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'week',
comparisonType: TimeRangeComparisonType.WeekBefore,
rangeTo: 'now',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expectTextsNotInDocument(component, ['Yesterday']);
expectTextsInDocument(component, ['A week ago']);
expectTextsNotInDocument(component, ['Day before']);
expectTextsInDocument(component, ['Week before']);
});
it('sets default values', () => {
const Wrapper = getWrapper({
@ -139,7 +140,7 @@ describe('TimeComparison', () => {
expect(spy).toHaveBeenCalledWith(expect.anything(), {
query: {
comparisonEnabled: 'true',
comparisonType: 'week',
comparisonType: TimeRangeComparisonType.WeekBefore,
},
});
});
@ -148,14 +149,14 @@ describe('TimeComparison', () => {
start: '2021-01-26T15:00:00.000Z',
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'week',
comparisonType: TimeRangeComparisonType.WeekBefore,
rangeTo: 'now',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expectTextsNotInDocument(component, ['Yesterday']);
expectTextsInDocument(component, ['A week ago']);
expectTextsNotInDocument(component, ['Day before']);
expectTextsInDocument(component, ['Week before']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
@ -167,13 +168,13 @@ describe('TimeComparison', () => {
start: '2021-01-26T15:00:00.000Z',
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'previousPeriod',
comparisonType: TimeRangeComparisonType.PeriodBefore,
rangeTo: '2021-01-28T15:00:00.000Z',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expectTextsInDocument(component, ['26/01 16:00 - 28/01 16:00']);
expectTextsInDocument(component, ['24/01 16:00 - 26/01 16:00']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
@ -187,14 +188,14 @@ describe('TimeComparison', () => {
start: '2021-01-20T15:00:00.000Z',
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'previousPeriod',
comparisonType: TimeRangeComparisonType.PeriodBefore,
rangeTo: 'now',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expect(spy).not.toHaveBeenCalled();
expectTextsInDocument(component, ['20/01 16:00 - 28/01 16:00']);
expectTextsInDocument(component, ['12/01 16:00 - 20/01 16:00']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
@ -206,14 +207,14 @@ describe('TimeComparison', () => {
start: '2020-12-20T15:00:00.000Z',
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'previousPeriod',
comparisonType: TimeRangeComparisonType.PeriodBefore,
rangeTo: 'now',
});
const component = render(<TimeComparison />, {
wrapper: Wrapper,
});
expect(spy).not.toHaveBeenCalled();
expectTextsInDocument(component, ['20/12/20 16:00 - 28/01/21 16:00']);
expectTextsInDocument(component, ['11/11/20 16:00 - 20/12/20 16:00']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex

View file

@ -16,6 +16,10 @@ import { useUrlParams } from '../../../context/url_params_context/use_url_params
import { px, unit } from '../../../style/variables';
import * as urlHelpers from '../../shared/Links/url_helpers';
import { useBreakPoints } from '../../../hooks/use_break_points';
import {
getTimeRangeComparison,
TimeRangeComparisonType,
} from './get_time_range_comparison';
const PrependContainer = euiStyled.div`
display: flex;
@ -25,15 +29,32 @@ const PrependContainer = euiStyled.div`
padding: 0 ${px(unit)};
`;
function formatPreviousPeriodDates({
momentStart,
momentEnd,
function getDateFormat({
previousPeriodStart,
currentPeriodEnd,
}: {
momentStart: moment.Moment;
momentEnd: moment.Moment;
previousPeriodStart?: string;
currentPeriodEnd?: string;
}) {
const isDifferentYears = momentStart.get('year') !== momentEnd.get('year');
const dateFormat = isDifferentYears ? 'DD/MM/YY HH:mm' : 'DD/MM HH:mm';
const momentPreviousPeriodStart = moment(previousPeriodStart);
const momentCurrentPeriodEnd = moment(currentPeriodEnd);
const isDifferentYears =
momentPreviousPeriodStart.get('year') !==
momentCurrentPeriodEnd.get('year');
return isDifferentYears ? 'DD/MM/YY HH:mm' : 'DD/MM HH:mm';
}
function formatDate({
dateFormat,
previousPeriodStart,
previousPeriodEnd,
}: {
dateFormat: string;
previousPeriodStart?: string;
previousPeriodEnd?: string;
}) {
const momentStart = moment(previousPeriodStart);
const momentEnd = moment(previousPeriodEnd);
return `${momentStart.format(dateFormat)} - ${momentEnd.format(dateFormat)}`;
}
@ -49,17 +70,17 @@ function getSelectOptions({
const momentStart = moment(start);
const momentEnd = moment(end);
const yesterdayOption = {
value: 'yesterday',
text: i18n.translate('xpack.apm.timeComparison.select.yesterday', {
defaultMessage: 'Yesterday',
const dayBeforeOption = {
value: TimeRangeComparisonType.DayBefore,
text: i18n.translate('xpack.apm.timeComparison.select.dayBefore', {
defaultMessage: 'Day before',
}),
};
const aWeekAgoOption = {
value: 'week',
text: i18n.translate('xpack.apm.timeComparison.select.weekAgo', {
defaultMessage: 'A week ago',
const weekBeforeOption = {
value: TimeRangeComparisonType.WeekBefore,
text: i18n.translate('xpack.apm.timeComparison.select.weekBefore', {
defaultMessage: 'Week before',
}),
};
@ -69,23 +90,39 @@ function getSelectOptions({
unitOfTime: 'days',
precise: true,
});
const isRangeToNow = rangeTo === 'now';
if (isRangeToNow) {
// Less than or equals to one day
if (dateDiff <= 1) {
return [yesterdayOption, aWeekAgoOption];
return [dayBeforeOption, weekBeforeOption];
}
// Less than or equals to one week
if (dateDiff <= 7) {
return [aWeekAgoOption];
return [weekBeforeOption];
}
}
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
comparisonType: TimeRangeComparisonType.PeriodBefore,
start,
end,
});
const dateFormat = getDateFormat({
previousPeriodStart: comparisonStart,
currentPeriodEnd: end,
});
const prevPeriodOption = {
value: 'previousPeriod',
text: formatPreviousPeriodDates({ momentStart, momentEnd }),
value: TimeRangeComparisonType.PeriodBefore,
text: formatDate({
dateFormat,
previousPeriodStart: comparisonStart,
previousPeriodEnd: comparisonEnd,
}),
};
// above one week or when rangeTo is not "now"

View file

@ -11,6 +11,7 @@ import { pickKeys } from '../../../common/utils/pick_keys';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config';
import { toQuery } from '../../components/shared/Links/url_helpers';
import { TimeRangeComparisonType } from '../../components/shared/time_comparison/get_time_range_comparison';
import {
getDateRange,
removeUndefinedProps,
@ -84,8 +85,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) {
comparisonEnabled: comparisonEnabled
? toBoolean(comparisonEnabled)
: undefined,
comparisonType,
comparisonType: comparisonType as TimeRangeComparisonType | undefined,
// ui filters
environment,
...localUIFilters,

View file

@ -7,6 +7,7 @@
import { LatencyAggregationType } from '../../../common/latency_aggregation_types';
import { LocalUIFilterName } from '../../../common/ui_filter';
import { TimeRangeComparisonType } from '../../components/shared/time_comparison/get_time_range_comparison';
export type IUrlParams = {
detailTab?: string;
@ -32,5 +33,5 @@ export type IUrlParams = {
percentile?: number;
latencyAggregationType?: LatencyAggregationType;
comparisonEnabled?: boolean;
comparisonType?: string;
comparisonType?: TimeRangeComparisonType;
} & Partial<Record<LocalUIFilterName, string>>;

View file

@ -6,7 +6,6 @@
*/
import { Logger } from 'kibana/server';
import moment from 'moment';
import { isActivePlatinumLicense } from '../../../common/license_check';
import { APMConfig } from '../..';
import { KibanaRequest } from '../../../../../../src/core/server';
@ -54,19 +53,19 @@ interface SetupRequestParams {
/**
* Timestamp in ms since epoch
*/
start?: string;
start?: number;
/**
* Timestamp in ms since epoch
*/
end?: string;
end?: number;
uiFilters?: string;
};
}
type InferSetup<TParams extends SetupRequestParams> = Setup &
(TParams extends { query: { start: string } } ? { start: number } : {}) &
(TParams extends { query: { end: string } } ? { end: number } : {});
(TParams extends { query: { start: number } } ? { start: number } : {}) &
(TParams extends { query: { end: number } } ? { end: number } : {});
export async function setupRequest<TParams extends SetupRequestParams>(
context: APMRequestHandlerContext<TParams>,
@ -115,8 +114,8 @@ export async function setupRequest<TParams extends SetupRequestParams>(
};
return {
...('start' in query ? { start: moment.utc(query.start).valueOf() } : {}),
...('end' in query ? { end: moment.utc(query.end).valueOf() } : {}),
...('start' in query ? { start: query.start } : {}),
...('end' in query ? { end: query.end } : {}),
...coreSetupRequest,
} as InferSetup<TParams>;
});

View file

@ -6,7 +6,6 @@
*/
import { ESFilter } from '../../../../../typings/elasticsearch';
import { PromiseReturnType } from '../../../../observability/typings/common';
import {
SERVICE_NAME,
TRANSACTION_TYPE,
@ -17,38 +16,27 @@ import {
getProcessorEventForAggregatedTransactions,
} from '../helpers/aggregated_transactions';
import { getBucketSize } from '../helpers/get_bucket_size';
import { calculateThroughput } from '../helpers/calculate_throughput';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { Setup } from '../helpers/setup_request';
import { withApmSpan } from '../../utils/with_apm_span';
interface Options {
searchAggregatedTransactions: boolean;
serviceName: string;
setup: Setup & SetupTimeRange;
setup: Setup;
transactionType: string;
start: number;
end: number;
}
type ESResponse = PromiseReturnType<typeof fetcher>;
function transform(options: Options, response: ESResponse) {
if (response.hits.total.value === 0) {
return [];
}
const { start, end } = options.setup;
const buckets = response.aggregations?.throughput.buckets ?? [];
return buckets.map(({ key: x, doc_count: value }) => ({
x,
y: calculateThroughput({ start, end, value }),
}));
}
async function fetcher({
function fetcher({
searchAggregatedTransactions,
serviceName,
setup,
transactionType,
start,
end,
}: Options) {
const { start, end, apmEventClient } = setup;
const { apmEventClient } = setup;
const { intervalString } = getBucketSize({ start, end });
const filter: ESFilter[] = [
{ term: { [SERVICE_NAME]: serviceName } },
@ -72,13 +60,20 @@ async function fetcher({
size: 0,
query: { bool: { filter } },
aggs: {
throughput: {
timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval: intervalString,
min_doc_count: 0,
extended_bounds: { min: start, max: end },
},
aggs: {
throughput: {
rate: {
unit: 'minute' as const,
},
},
},
},
},
},
@ -89,8 +84,15 @@ async function fetcher({
export function getThroughput(options: Options) {
return withApmSpan('get_throughput_for_service', async () => {
return {
throughput: transform(options, await fetcher(options)),
};
const response = await fetcher(options);
return (
response.aggregations?.timeseries.buckets.map((bucket) => {
return {
x: bucket.key,
y: bucket.throughput.value,
};
}) ?? []
);
});
}

View file

@ -6,11 +6,16 @@
*/
import * as t from 'io-ts';
import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt';
import { isoToEpochRt } from '../../common/runtime_types/iso_to_epoch_rt';
export const rangeRt = t.type({
start: dateAsStringRt,
end: dateAsStringRt,
start: isoToEpochRt,
end: isoToEpochRt,
});
export const comparisonRangeRt = t.partial({
comparisonStart: isoToEpochRt,
comparisonEnd: isoToEpochRt,
});
export const uiFiltersRt = t.type({ uiFilters: t.string });

View file

@ -5,26 +5,27 @@
* 2.0.
*/
import * as t from 'io-ts';
import Boom from '@hapi/boom';
import * as t from 'io-ts';
import { uniq } from 'lodash';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceAgentName } from '../lib/services/get_service_agent_name';
import { getServices } from '../lib/services/get_services';
import { getServiceTransactionTypes } from '../lib/services/get_service_transaction_types';
import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata';
import { createRoute } from './create_route';
import { uiFiltersRt, rangeRt } from './default_api_types';
import { getServiceAnnotations } from '../lib/services/annotations';
import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { getServiceErrorGroups } from '../lib/services/get_service_error_groups';
import { getServiceDependencies } from '../lib/services/get_service_dependencies';
import { isoToEpochRt } from '../../common/runtime_types/iso_to_epoch_rt';
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
import { getThroughput } from '../lib/services/get_throughput';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceAnnotations } from '../lib/services/annotations';
import { getServices } from '../lib/services/get_services';
import { getServiceAgentName } from '../lib/services/get_service_agent_name';
import { getServiceDependencies } from '../lib/services/get_service_dependencies';
import { getServiceErrorGroups } from '../lib/services/get_service_error_groups';
import { getServiceInstances } from '../lib/services/get_service_instances';
import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details';
import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons';
import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata';
import { getServiceTransactionTypes } from '../lib/services/get_service_transaction_types';
import { getThroughput } from '../lib/services/get_throughput';
import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate';
import { createRoute } from './create_route';
import { comparisonRangeRt, rangeRt, uiFiltersRt } from './default_api_types';
import { withApmSpan } from '../utils/with_apm_span';
export const servicesRoute = createRoute({
@ -216,7 +217,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
}),
body: t.intersection([
t.type({
'@timestamp': dateAsStringRt,
'@timestamp': isoToEpochRt,
service: t.intersection([
t.type({
version: t.string,
@ -251,6 +252,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
annotationsClient.create({
message: body.service.version,
...body,
'@timestamp': new Date(body['@timestamp']).toISOString(),
annotation: {
type: 'deployment',
},
@ -325,23 +327,56 @@ export const serviceThroughputRoute = createRoute({
t.type({ transactionType: t.string }),
uiFiltersRt,
rangeRt,
comparisonRangeRt,
]),
}),
options: { tags: ['access:apm'] },
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
const { serviceName } = context.params.path;
const { transactionType } = context.params.query;
const {
transactionType,
comparisonStart,
comparisonEnd,
} = context.params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
return getThroughput({
const { start, end } = setup;
const commonProps = {
searchAggregatedTransactions,
serviceName,
setup,
transactionType,
});
};
const [currentPeriod, previousPeriod] = await Promise.all([
getThroughput({
...commonProps,
start,
end,
}),
comparisonStart && comparisonEnd
? getThroughput({
...commonProps,
start: comparisonStart,
end: comparisonEnd,
}).then((coordinates) =>
offsetPreviousPeriodCoordinates({
currentPeriodStart: start,
previousPeriodStart: comparisonStart,
previousPeriodTimeseries: coordinates,
})
)
: [],
]);
return {
currentPeriod,
previousPeriod,
};
},
});

View file

@ -7,6 +7,7 @@
import * as t from 'io-ts';
import Boom from '@hapi/boom';
import { toBooleanRt } from '../../../common/runtime_types/to_boolean_rt';
import { setupRequest } from '../../lib/helpers/setup_request';
import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names';
import { createOrUpdateConfiguration } from '../../lib/settings/agent_configuration/create_or_update_configuration';
@ -22,7 +23,6 @@ import {
serviceRt,
agentConfigurationIntakeRt,
} from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt';
import { jsonRt } from '../../../common/runtime_types/json_rt';
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
// get list of configurations
@ -103,7 +103,7 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({
tags: ['access:apm', 'access:apm_write'],
},
params: t.intersection([
t.partial({ query: t.partial({ overwrite: jsonRt.pipe(t.boolean) }) }),
t.partial({ query: t.partial({ overwrite: toBooleanRt }) }),
t.type({ body: agentConfigurationIntakeRt }),
]),
handler: async ({ context, request }) => {

View file

@ -143,7 +143,7 @@ export type Client<
forceCache?: boolean;
endpoint: TEndpoint;
} & (TRouteState[TEndpoint] extends { params: t.Any }
? MaybeOptional<{ params: t.TypeOf<TRouteState[TEndpoint]['params']> }>
? MaybeOptional<{ params: t.OutputOf<TRouteState[TEndpoint]['params']> }>
: {}) &
(TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
) => Promise<

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Coordinate } from '../../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();
describe('mergePeriodsTimeseries', () => {
describe('returns empty array', () => {
it('when previous timeseries is not defined', () => {
expect(
offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
previousPeriodTimeseries: undefined,
})
).toEqual([]);
});
it('when previous timeseries is empty', () => {
expect(
offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
previousPeriodTimeseries: [],
})
).toEqual([]);
});
});
it('offsets previous period timeseries', () => {
const previousPeriodTimeseries: Coordinate[] = [
{ x: new Date('2021-01-27T14:45:00.000Z').valueOf(), y: 1 },
{ x: new Date('2021-01-27T15:00:00.000Z').valueOf(), y: 2 },
{ x: new Date('2021-01-27T15:15:00.000Z').valueOf(), y: 2 },
{ x: new Date('2021-01-27T15:30:00.000Z').valueOf(), y: 3 },
];
expect(
offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
previousPeriodTimeseries,
})
).toEqual([
{ x: new Date('2021-01-28T14:45:00.000Z').valueOf(), y: 1 },
{ x: new Date('2021-01-28T15:00:00.000Z').valueOf(), y: 2 },
{ x: new Date('2021-01-28T15:15:00.000Z').valueOf(), y: 2 },
{ x: new Date('2021-01-28T15:30:00.000Z').valueOf(), y: 3 },
]);
});
});

View file

@ -0,0 +1,35 @@
/*
* 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 moment from 'moment';
import { Coordinate } from '../../typings/timeseries';
export function offsetPreviousPeriodCoordinates({
currentPeriodStart,
previousPeriodStart,
previousPeriodTimeseries,
}: {
currentPeriodStart: number;
previousPeriodStart: number;
previousPeriodTimeseries?: Coordinate[];
}) {
if (!previousPeriodTimeseries) {
return [];
}
const dateOffset = moment(currentPeriodStart).diff(
moment(previousPeriodStart)
);
return previousPeriodTimeseries.map(({ x, y }) => {
const offsetX = moment(x).add(dateOffset).valueOf();
return {
x: offsetX,
y,
};
});
}

View file

@ -8,7 +8,7 @@ Array [
},
Object {
"x": 1607435880000,
"y": 0.133333333333333,
"y": 8,
},
Object {
"x": 1607435910000,
@ -16,7 +16,7 @@ Array [
},
Object {
"x": 1607435940000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607435970000,
@ -24,11 +24,11 @@ Array [
},
Object {
"x": 1607436000000,
"y": 0.1,
"y": 6,
},
Object {
"x": 1607436030000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607436060000,
@ -40,7 +40,7 @@ Array [
},
Object {
"x": 1607436120000,
"y": 0.133333333333333,
"y": 8,
},
Object {
"x": 1607436150000,
@ -56,7 +56,7 @@ Array [
},
Object {
"x": 1607436240000,
"y": 0.2,
"y": 12,
},
Object {
"x": 1607436270000,
@ -68,15 +68,15 @@ Array [
},
Object {
"x": 1607436330000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607436360000,
"y": 0.166666666666667,
"y": 10,
},
Object {
"x": 1607436390000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607436420000,
@ -88,11 +88,11 @@ Array [
},
Object {
"x": 1607436480000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607436510000,
"y": 0.166666666666667,
"y": 10,
},
Object {
"x": 1607436540000,
@ -104,11 +104,11 @@ Array [
},
Object {
"x": 1607436600000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607436630000,
"y": 0.233333333333333,
"y": 14,
},
Object {
"x": 1607436660000,
@ -124,7 +124,7 @@ Array [
},
Object {
"x": 1607436750000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607436780000,
@ -132,15 +132,15 @@ Array [
},
Object {
"x": 1607436810000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607436840000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607436870000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607436900000,
@ -152,11 +152,11 @@ Array [
},
Object {
"x": 1607436960000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607436990000,
"y": 0.133333333333333,
"y": 8,
},
Object {
"x": 1607437020000,
@ -168,11 +168,11 @@ Array [
},
Object {
"x": 1607437080000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607437110000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607437140000,
@ -184,15 +184,15 @@ Array [
},
Object {
"x": 1607437200000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607437230000,
"y": 0.233333333333333,
"y": 14,
},
Object {
"x": 1607437260000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607437290000,
@ -200,11 +200,11 @@ Array [
},
Object {
"x": 1607437320000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607437350000,
"y": 0.0666666666666667,
"y": 4,
},
Object {
"x": 1607437380000,
@ -216,11 +216,11 @@ Array [
},
Object {
"x": 1607437440000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607437470000,
"y": 0.1,
"y": 6,
},
Object {
"x": 1607437500000,
@ -232,7 +232,7 @@ Array [
},
Object {
"x": 1607437560000,
"y": 0.0333333333333333,
"y": 2,
},
Object {
"x": 1607437590000,
@ -248,3 +248,740 @@ Array [
},
]
`;
exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded with time comparison has the correct throughput 1`] = `
Object {
"currentPeriod": Array [
Object {
"x": 1607436770000,
"y": 0,
},
Object {
"x": 1607436780000,
"y": 0,
},
Object {
"x": 1607436790000,
"y": 0,
},
Object {
"x": 1607436800000,
"y": 0,
},
Object {
"x": 1607436810000,
"y": 0,
},
Object {
"x": 1607436820000,
"y": 6,
},
Object {
"x": 1607436830000,
"y": 0,
},
Object {
"x": 1607436840000,
"y": 0,
},
Object {
"x": 1607436850000,
"y": 0,
},
Object {
"x": 1607436860000,
"y": 6,
},
Object {
"x": 1607436870000,
"y": 6,
},
Object {
"x": 1607436880000,
"y": 6,
},
Object {
"x": 1607436890000,
"y": 0,
},
Object {
"x": 1607436900000,
"y": 0,
},
Object {
"x": 1607436910000,
"y": 0,
},
Object {
"x": 1607436920000,
"y": 0,
},
Object {
"x": 1607436930000,
"y": 0,
},
Object {
"x": 1607436940000,
"y": 0,
},
Object {
"x": 1607436950000,
"y": 0,
},
Object {
"x": 1607436960000,
"y": 0,
},
Object {
"x": 1607436970000,
"y": 0,
},
Object {
"x": 1607436980000,
"y": 12,
},
Object {
"x": 1607436990000,
"y": 6,
},
Object {
"x": 1607437000000,
"y": 18,
},
Object {
"x": 1607437010000,
"y": 0,
},
Object {
"x": 1607437020000,
"y": 0,
},
Object {
"x": 1607437030000,
"y": 0,
},
Object {
"x": 1607437040000,
"y": 0,
},
Object {
"x": 1607437050000,
"y": 0,
},
Object {
"x": 1607437060000,
"y": 0,
},
Object {
"x": 1607437070000,
"y": 0,
},
Object {
"x": 1607437080000,
"y": 0,
},
Object {
"x": 1607437090000,
"y": 0,
},
Object {
"x": 1607437100000,
"y": 6,
},
Object {
"x": 1607437110000,
"y": 6,
},
Object {
"x": 1607437120000,
"y": 0,
},
Object {
"x": 1607437130000,
"y": 0,
},
Object {
"x": 1607437140000,
"y": 0,
},
Object {
"x": 1607437150000,
"y": 0,
},
Object {
"x": 1607437160000,
"y": 0,
},
Object {
"x": 1607437170000,
"y": 0,
},
Object {
"x": 1607437180000,
"y": 0,
},
Object {
"x": 1607437190000,
"y": 0,
},
Object {
"x": 1607437200000,
"y": 0,
},
Object {
"x": 1607437210000,
"y": 0,
},
Object {
"x": 1607437220000,
"y": 12,
},
Object {
"x": 1607437230000,
"y": 30,
},
Object {
"x": 1607437240000,
"y": 12,
},
Object {
"x": 1607437250000,
"y": 0,
},
Object {
"x": 1607437260000,
"y": 0,
},
Object {
"x": 1607437270000,
"y": 6,
},
Object {
"x": 1607437280000,
"y": 0,
},
Object {
"x": 1607437290000,
"y": 0,
},
Object {
"x": 1607437300000,
"y": 0,
},
Object {
"x": 1607437310000,
"y": 0,
},
Object {
"x": 1607437320000,
"y": 0,
},
Object {
"x": 1607437330000,
"y": 0,
},
Object {
"x": 1607437340000,
"y": 6,
},
Object {
"x": 1607437350000,
"y": 0,
},
Object {
"x": 1607437360000,
"y": 12,
},
Object {
"x": 1607437370000,
"y": 0,
},
Object {
"x": 1607437380000,
"y": 0,
},
Object {
"x": 1607437390000,
"y": 0,
},
Object {
"x": 1607437400000,
"y": 0,
},
Object {
"x": 1607437410000,
"y": 0,
},
Object {
"x": 1607437420000,
"y": 0,
},
Object {
"x": 1607437430000,
"y": 0,
},
Object {
"x": 1607437440000,
"y": 0,
},
Object {
"x": 1607437450000,
"y": 0,
},
Object {
"x": 1607437460000,
"y": 6,
},
Object {
"x": 1607437470000,
"y": 12,
},
Object {
"x": 1607437480000,
"y": 6,
},
Object {
"x": 1607437490000,
"y": 0,
},
Object {
"x": 1607437500000,
"y": 0,
},
Object {
"x": 1607437510000,
"y": 0,
},
Object {
"x": 1607437520000,
"y": 0,
},
Object {
"x": 1607437530000,
"y": 0,
},
Object {
"x": 1607437540000,
"y": 0,
},
Object {
"x": 1607437550000,
"y": 0,
},
Object {
"x": 1607437560000,
"y": 0,
},
Object {
"x": 1607437570000,
"y": 6,
},
Object {
"x": 1607437580000,
"y": 0,
},
Object {
"x": 1607437590000,
"y": 0,
},
Object {
"x": 1607437600000,
"y": 0,
},
Object {
"x": 1607437610000,
"y": 0,
},
Object {
"x": 1607437620000,
"y": 0,
},
Object {
"x": 1607437630000,
"y": 0,
},
Object {
"x": 1607437640000,
"y": 0,
},
Object {
"x": 1607437650000,
"y": 0,
},
Object {
"x": 1607437660000,
"y": 0,
},
Object {
"x": 1607437670000,
"y": 0,
},
],
"previousPeriod": Array [
Object {
"x": 1607436770000,
"y": 0,
},
Object {
"x": 1607436780000,
"y": 0,
},
Object {
"x": 1607436790000,
"y": 0,
},
Object {
"x": 1607436800000,
"y": 24,
},
Object {
"x": 1607436810000,
"y": 0,
},
Object {
"x": 1607436820000,
"y": 0,
},
Object {
"x": 1607436830000,
"y": 0,
},
Object {
"x": 1607436840000,
"y": 12,
},
Object {
"x": 1607436850000,
"y": 0,
},
Object {
"x": 1607436860000,
"y": 0,
},
Object {
"x": 1607436870000,
"y": 0,
},
Object {
"x": 1607436880000,
"y": 0,
},
Object {
"x": 1607436890000,
"y": 0,
},
Object {
"x": 1607436900000,
"y": 0,
},
Object {
"x": 1607436910000,
"y": 12,
},
Object {
"x": 1607436920000,
"y": 6,
},
Object {
"x": 1607436930000,
"y": 6,
},
Object {
"x": 1607436940000,
"y": 0,
},
Object {
"x": 1607436950000,
"y": 0,
},
Object {
"x": 1607436960000,
"y": 0,
},
Object {
"x": 1607436970000,
"y": 0,
},
Object {
"x": 1607436980000,
"y": 0,
},
Object {
"x": 1607436990000,
"y": 0,
},
Object {
"x": 1607437000000,
"y": 0,
},
Object {
"x": 1607437010000,
"y": 0,
},
Object {
"x": 1607437020000,
"y": 0,
},
Object {
"x": 1607437030000,
"y": 6,
},
Object {
"x": 1607437040000,
"y": 18,
},
Object {
"x": 1607437050000,
"y": 0,
},
Object {
"x": 1607437060000,
"y": 0,
},
Object {
"x": 1607437070000,
"y": 0,
},
Object {
"x": 1607437080000,
"y": 0,
},
Object {
"x": 1607437090000,
"y": 0,
},
Object {
"x": 1607437100000,
"y": 0,
},
Object {
"x": 1607437110000,
"y": 0,
},
Object {
"x": 1607437120000,
"y": 0,
},
Object {
"x": 1607437130000,
"y": 0,
},
Object {
"x": 1607437140000,
"y": 0,
},
Object {
"x": 1607437150000,
"y": 0,
},
Object {
"x": 1607437160000,
"y": 36,
},
Object {
"x": 1607437170000,
"y": 0,
},
Object {
"x": 1607437180000,
"y": 0,
},
Object {
"x": 1607437190000,
"y": 0,
},
Object {
"x": 1607437200000,
"y": 0,
},
Object {
"x": 1607437210000,
"y": 0,
},
Object {
"x": 1607437220000,
"y": 0,
},
Object {
"x": 1607437230000,
"y": 0,
},
Object {
"x": 1607437240000,
"y": 6,
},
Object {
"x": 1607437250000,
"y": 0,
},
Object {
"x": 1607437260000,
"y": 0,
},
Object {
"x": 1607437270000,
"y": 0,
},
Object {
"x": 1607437280000,
"y": 30,
},
Object {
"x": 1607437290000,
"y": 6,
},
Object {
"x": 1607437300000,
"y": 0,
},
Object {
"x": 1607437310000,
"y": 0,
},
Object {
"x": 1607437320000,
"y": 0,
},
Object {
"x": 1607437330000,
"y": 0,
},
Object {
"x": 1607437340000,
"y": 0,
},
Object {
"x": 1607437350000,
"y": 0,
},
Object {
"x": 1607437360000,
"y": 0,
},
Object {
"x": 1607437370000,
"y": 0,
},
Object {
"x": 1607437380000,
"y": 0,
},
Object {
"x": 1607437390000,
"y": 0,
},
Object {
"x": 1607437400000,
"y": 12,
},
Object {
"x": 1607437410000,
"y": 6,
},
Object {
"x": 1607437420000,
"y": 24,
},
Object {
"x": 1607437430000,
"y": 0,
},
Object {
"x": 1607437440000,
"y": 0,
},
Object {
"x": 1607437450000,
"y": 0,
},
Object {
"x": 1607437460000,
"y": 0,
},
Object {
"x": 1607437470000,
"y": 0,
},
Object {
"x": 1607437480000,
"y": 0,
},
Object {
"x": 1607437490000,
"y": 0,
},
Object {
"x": 1607437500000,
"y": 0,
},
Object {
"x": 1607437510000,
"y": 0,
},
Object {
"x": 1607437520000,
"y": 12,
},
Object {
"x": 1607437530000,
"y": 30,
},
Object {
"x": 1607437540000,
"y": 12,
},
Object {
"x": 1607437550000,
"y": 0,
},
Object {
"x": 1607437560000,
"y": 0,
},
Object {
"x": 1607437570000,
"y": 0,
},
Object {
"x": 1607437580000,
"y": 0,
},
Object {
"x": 1607437590000,
"y": 0,
},
Object {
"x": 1607437600000,
"y": 0,
},
Object {
"x": 1607437610000,
"y": 0,
},
Object {
"x": 1607437620000,
"y": 0,
},
Object {
"x": 1607437630000,
"y": 0,
},
Object {
"x": 1607437640000,
"y": 0,
},
Object {
"x": 1607437650000,
"y": 6,
},
Object {
"x": 1607437660000,
"y": 6,
},
Object {
"x": 1607437670000,
"y": 0,
},
],
}
`;

View file

@ -8,10 +8,15 @@
import expect from '@kbn/expect';
import qs from 'querystring';
import { first, last } from 'lodash';
import moment from 'moment';
import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number';
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';
type ThroughputReturn = APIReturnType<'GET /api/apm/services/{serviceName}/throughput'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@ -29,17 +34,16 @@ export default function ApiTest({ getService }: FtrProviderContext) {
})}`
);
expect(response.status).to.be(200);
expect(response.body.throughput.length).to.be(0);
expect(response.body.currentPeriod.length).to.be(0);
expect(response.body.previousPeriod.length).to.be(0);
});
});
let throughputResponse: ThroughputReturn;
registry.when(
'Throughput when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
let throughputResponse: {
throughput: Array<{ x: number; y: number | null }>;
};
before(async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-java/throughput?${qs.stringify({
@ -53,31 +57,98 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('returns some data', () => {
expect(throughputResponse.throughput.length).to.be.greaterThan(0);
expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0);
expect(throughputResponse.previousPeriod.length).not.to.be.greaterThan(0);
const nonNullDataPoints = throughputResponse.throughput.filter(({ y }) => y !== null);
const nonNullDataPoints = throughputResponse.currentPeriod.filter(({ y }) =>
isFiniteNumber(y)
);
expect(nonNullDataPoints.length).to.be.greaterThan(0);
});
it('has the correct start date', () => {
expectSnapshot(
new Date(first(throughputResponse.throughput)?.x ?? NaN).toISOString()
new Date(first(throughputResponse.currentPeriod)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-12-08T13:57:30.000Z"`);
});
it('has the correct end date', () => {
expectSnapshot(
new Date(last(throughputResponse.throughput)?.x ?? NaN).toISOString()
new Date(last(throughputResponse.currentPeriod)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-12-08T14:27:30.000Z"`);
});
it('has the correct number of buckets', () => {
expectSnapshot(throughputResponse.throughput.length).toMatchInline(`61`);
expectSnapshot(throughputResponse.currentPeriod.length).toMatchInline(`61`);
});
it('has the correct throughput', () => {
expectSnapshot(throughputResponse.throughput).toMatch();
expectSnapshot(throughputResponse.currentPeriod).toMatch();
});
}
);
registry.when(
'Throughput when data is loaded with time comparison',
{ config: 'basic', archives: [archiveName] },
() => {
before(async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-java/throughput?${qs.stringify({
uiFilters: encodeURIComponent('{}'),
transactionType: 'request',
start: moment(metadata.end).subtract(15, 'minutes').toISOString(),
end: metadata.end,
comparisonStart: metadata.start,
comparisonEnd: moment(metadata.start).add(15, 'minutes').toISOString(),
})}`
);
throughputResponse = response.body;
});
it('returns some data', () => {
expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0);
expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0);
const currentPeriodNonNullDataPoints = throughputResponse.currentPeriod.filter(({ y }) =>
isFiniteNumber(y)
);
const previousPeriodNonNullDataPoints = throughputResponse.previousPeriod.filter(({ y }) =>
isFiniteNumber(y)
);
expect(currentPeriodNonNullDataPoints.length).to.be.greaterThan(0);
expect(previousPeriodNonNullDataPoints.length).to.be.greaterThan(0);
});
it('has the correct start date', () => {
expectSnapshot(
new Date(first(throughputResponse.currentPeriod)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-12-08T14:12:50.000Z"`);
expectSnapshot(
new Date(first(throughputResponse.previousPeriod)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-12-08T14:12:50.000Z"`);
});
it('has the correct end date', () => {
expectSnapshot(
new Date(last(throughputResponse.currentPeriod)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-12-08T14:27:50.000Z"`);
expectSnapshot(
new Date(last(throughputResponse.previousPeriod)?.x ?? NaN).toISOString()
).toMatchInline(`"2020-12-08T14:27:50.000Z"`);
});
it('has the correct number of buckets', () => {
expectSnapshot(throughputResponse.currentPeriod.length).toMatchInline(`91`);
expectSnapshot(throughputResponse.previousPeriod.length).toMatchInline(`91`);
});
it('has the correct throughput', () => {
expectSnapshot(throughputResponse).toMatch();
});
}
);