[APM] Latency chart for overview (#84634)

* adding latency chart

* adding loading indicator

* fixing unit test

* fixing y-axis format

* fixing some stuff

* using urlhelpers

* adding latency aggregation type on the transactions overview api

* fixing transaction group overview route

* fixing merge problems

* breaking /transactions/charts into /latency and /thoughput

* adding unit tests

* fixing UI

* fixing i18n

* addressing pr comments

* fix api test

* refactoring some stuff

* addressing pr comments

* addressing pr comments

* adding latecy chart

* fixing test

* fixing api tests

* fixing api test

* fixing build

* addressing pr comments

* addressing pr comments

* refactoring some stuff

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2020-12-14 16:45:54 +01:00 committed by GitHub
parent 0dfcbe92ed
commit 542a8aa1d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 716 additions and 555 deletions

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
export enum LatencyAggregationType {
avg = 'avg',
p99 = 'p99',
p95 = 'p95',
}
export const latencyAggregationTypeRt = t.union([
t.literal('avg'),
t.literal('p95'),
t.literal('p99'),
]);

View file

@ -15,12 +15,14 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
import { isRumAgentName } from '../../../../common/agent_name';
import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { LatencyChart } from '../../shared/charts/latency_chart';
import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart';
import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart';
import { SearchBar } from '../../shared/search_bar';
import { ServiceOverviewErrorsTable } from './service_overview_errors_table';
import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table';
import { ServiceOverviewErrorsTable } from './service_overview_errors_table';
import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart';
import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table';
@ -43,99 +45,96 @@ export function ServiceOverview({
useTrackPageview({ app: 'apm', path: 'service_overview', delay: 15000 });
return (
<ChartPointerEventContextProvider>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.latencyChartTitle',
{
defaultMessage: 'Latency',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<ServiceOverviewThroughputChart height={chartHeight} />
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<ServiceOverviewTransactionsTable serviceName={serviceName} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
{!isRumAgentName(agentName) && (
<AnnotationsContextProvider>
<ChartPointerEventContextProvider>
<SearchBar />
<EuiPage>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiPanel>
<LatencyChart height={200} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<TransactionErrorRateChart
height={chartHeight}
showAnnotations={false}
/>
<ServiceOverviewThroughputChart height={chartHeight} />
</EuiFlexItem>
)}
<EuiFlexItem grow={6}>
<EuiPanel>
<ServiceOverviewErrorsTable serviceName={serviceName} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<TransactionBreakdownChart showAnnotations={false} />
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<ServiceOverviewDependenciesTable serviceName={serviceName} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle',
{
defaultMessage: 'Instances latency distribution',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.instancesTableTitle',
{
defaultMessage: 'Instances',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPage>
</ChartPointerEventContextProvider>
<EuiFlexItem grow={6}>
<EuiPanel>
<ServiceOverviewTransactionsTable
serviceName={serviceName}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
{!isRumAgentName(agentName) && (
<EuiFlexItem grow={4}>
<TransactionErrorRateChart
height={chartHeight}
showAnnotations={false}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={6}>
<EuiPanel>
<ServiceOverviewErrorsTable serviceName={serviceName} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<TransactionBreakdownChart showAnnotations={false} />
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<ServiceOverviewDependenciesTable
serviceName={serviceName}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle',
{
defaultMessage: 'Instances latency distribution',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.instancesTableTitle',
{
defaultMessage: 'Instances',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPage>
</ChartPointerEventContextProvider>
</AnnotationsContextProvider>
);
}

View file

@ -15,6 +15,8 @@ import React, { useState } from 'react';
import styled from 'styled-components';
import { EuiToolTip } from '@elastic/eui';
import { ValuesType } from 'utility-types';
import { useLatencyAggregationType } from '../../../../hooks/use_latency_Aggregation_type';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import {
asDuration,
asPercent,
@ -64,9 +66,39 @@ const StyledTransactionDetailLink = styled(TransactionDetailLink)`
${truncate('100%')}
`;
export function ServiceOverviewTransactionsTable(props: Props) {
const { serviceName } = props;
function getLatencyAggregationTypeLabel(
latencyAggregationType?: LatencyAggregationType
) {
switch (latencyAggregationType) {
case 'p95': {
return i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95',
{
defaultMessage: 'Latency (95th)',
}
);
}
case 'p99': {
return i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnLatency.p99',
{
defaultMessage: 'Latency (99th)',
}
);
}
default: {
return i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg',
{
defaultMessage: 'Latency (avg.)',
}
);
}
}
}
export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
const latencyAggregationType = useLatencyAggregationType();
const {
uiFilters,
urlParams: { start, end },
@ -94,7 +126,7 @@ export function ServiceOverviewTransactionsTable(props: Props) {
},
status,
} = useFetcher(() => {
if (!start || !end) {
if (!start || !end || !latencyAggregationType) {
return;
}
@ -112,6 +144,7 @@ export function ServiceOverviewTransactionsTable(props: Props) {
pageIndex: tableOptions.pageIndex,
sortField: tableOptions.sort.field,
sortDirection: tableOptions.sort.direction,
latencyAggregationType,
},
},
}).then((response) => {
@ -135,6 +168,7 @@ export function ServiceOverviewTransactionsTable(props: Props) {
tableOptions.pageIndex,
tableOptions.sort.field,
tableOptions.sort.direction,
latencyAggregationType,
]);
const {
@ -170,12 +204,7 @@ export function ServiceOverviewTransactionsTable(props: Props) {
},
{
field: 'latency',
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnLatency',
{
defaultMessage: 'Latency',
}
),
name: getLatencyAggregationTypeLabel(latencyAggregationType),
width: px(unit * 10),
render: (_, { latency }) => {
return (

View file

@ -37,7 +37,7 @@ export const PERSISTENT_APM_PARAMS: Array<keyof APMQueryParams> = [
*/
export function useAPMHref(
path: string,
persistentFilters: Array<keyof APMQueryParams> = PERSISTENT_APM_PARAMS
persistentFilters: Array<keyof APMQueryParams> = []
) {
const { urlParams } = useUrlParams();
const { basePath } = useApmPluginContext().core.http;

View file

@ -15,6 +15,7 @@ const persistedFilters: Array<keyof APMQueryParams> = [
'containerId',
'podName',
'serviceVersion',
'latencyAggregationType',
];
export function useTransactionOverviewHref(serviceName: string) {

View file

@ -9,19 +9,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { pickKeys } from '../../../../../common/utils/pick_keys';
import { APMQueryParams } from '../url_helpers';
import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink';
interface ServiceOverviewLinkProps extends APMLinkExtendProps {
serviceName: string;
}
const persistedFilters: Array<keyof APMQueryParams> = [
'latencyAggregationType',
];
export function useServiceOverviewHref(serviceName: string) {
return useAPMHref(`/services/${serviceName}/overview`);
return useAPMHref(`/services/${serviceName}/overview`, persistedFilters);
}
export function ServiceOverviewLink({
serviceName,
...rest
}: ServiceOverviewLinkProps) {
return <APMLink path={`/services/${serviceName}/overview`} {...rest} />;
const { urlParams } = useUrlParams();
return (
<APMLink
path={`/services/${serviceName}/overview`}
query={pickKeys(urlParams as APMQueryParams, ...persistedFilters)}
{...rest}
/>
);
}

View file

@ -6,6 +6,7 @@
import { History } from 'history';
import { parse, stringify } from 'query-string';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { url } from '../../../../../../../src/plugins/kibana_utils/public';
import { LocalUIFilterName } from '../../../../common/ui_filter';
@ -84,6 +85,7 @@ export type APMQueryParams = {
refreshInterval?: string | number;
searchTerm?: string;
percentile?: 50 | 75 | 90 | 95 | 99;
latencyAggregationType?: LatencyAggregationType;
} & { [key in LocalUIFilterName]?: string };
// forces every value of T[K] to be type: string

View file

@ -0,0 +1,108 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
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 { useTransactionLatencyChartsFetcher } from '../../../../hooks/use_transaction_latency_chart_fetcher';
import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
import {
getMaxY,
getResponseTimeTickFormatter,
} from '../../../shared/charts/transaction_charts/helper';
import { MLHeader } from '../../../shared/charts/transaction_charts/ml_header';
import * as urlHelpers from '../../../shared/Links/url_helpers';
interface Props {
height?: number;
}
const options: Array<{ value: LatencyAggregationType; text: string }> = [
{ value: LatencyAggregationType.avg, text: 'Average' },
{ value: LatencyAggregationType.p95, text: '95th percentile' },
{ value: LatencyAggregationType.p99, text: '99th percentile' },
];
export function LatencyChart({ height }: Props) {
const history = useHistory();
const { urlParams } = useUrlParams();
const { latencyAggregationType } = urlParams;
const license = useLicenseContext();
const {
latencyChartsData,
latencyChartsStatus,
} = useTransactionLatencyChartsFetcher();
const { latencyTimeseries, anomalyTimeseries, mlJobId } = latencyChartsData;
const latencyMaxY = getMaxY(latencyTimeseries);
const latencyFormatter = getDurationFormatter(latencyMaxY);
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.latencyChartTitle',
{
defaultMessage: 'Latency',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSelect
compressed
prepend={i18n.translate(
'xpack.apm.serviceOverview.latencyChartTitle.prepend',
{ defaultMessage: 'Metric' }
)}
options={options}
value={latencyAggregationType}
onChange={(nextOption) => {
urlHelpers.push(history, {
query: {
latencyAggregationType: nextOption.target.value,
},
});
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<MLHeader
hasValidMlLicense={license?.getFeature('ml').isAvailable}
mlJobId={mlJobId}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<TimeseriesChart
height={height}
fetchStatus={latencyChartsStatus}
id="latencyChart"
timeseries={latencyTimeseries}
yLabelFormat={getResponseTimeTickFormatter(latencyFormatter)}
anomalySeries={anomalyTimeseries}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -10,7 +10,7 @@ import {
getMaxY,
} from './helper';
import { TimeSeries } from '../../../../../typings/timeseries';
import { TimeSeries, Coordinate } from '../../../../../typings/timeseries';
import {
getDurationFormatter,
toMicroseconds,
@ -51,7 +51,7 @@ describe('transaction chart helper', () => {
it('returns zero for invalid y coordinate', () => {
const timeSeries = ([
{ data: [{ x: 1 }, { x: 2 }, { x: 3, y: -1 }] },
] as unknown) as TimeSeries[];
] as unknown) as Array<TimeSeries<Coordinate>>;
expect(getMaxY(timeSeries)).toEqual(0);
});
it('returns the max y coordinate', () => {
@ -63,7 +63,7 @@ describe('transaction chart helper', () => {
{ x: 3, y: 1 },
],
},
] as unknown) as TimeSeries[];
] as unknown) as Array<TimeSeries<Coordinate>>;
expect(getMaxY(timeSeries)).toEqual(10);
});
});

View file

@ -4,11 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { flatten } from 'lodash';
import { TimeFormatter } from '../../../../../common/utils/formatters';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { TimeFormatter } from '../../../../../common/utils/formatters';
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
import { TimeSeries, Coordinate } from '../../../../../typings/timeseries';
export function getResponseTimeTickFormatter(formatter: TimeFormatter) {
return (t: number) => {
@ -24,12 +23,11 @@ export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) {
};
}
export function getMaxY(timeSeries: TimeSeries[]) {
const coordinates = flatten(
timeSeries.map((serie: TimeSeries) => serie.data as Coordinate[])
);
const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0));
return Math.max(...numbers, 0);
export function getMaxY(timeSeries?: Array<TimeSeries<Coordinate>>) {
if (timeSeries) {
const coordinates = timeSeries.flatMap((serie) => serie.data);
const numbers = coordinates.map((c) => (c.y ? c.y : 0));
return Math.max(...numbers, 0);
}
return 0;
}

View file

@ -6,7 +6,6 @@
import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
@ -14,44 +13,28 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
TRANSACTION_ROUTE_CHANGE,
} from '../../../../../common/transaction_types';
import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types';
import { asTransactionRate } from '../../../../../common/utils/formatters';
import { AnnotationsContextProvider } from '../../../../context/annotations/annotations_context';
import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context';
import { LicenseContext } from '../../../../context/license/license_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useTransactionLatencyChartsFetcher } from '../../../../hooks/use_transaction_latency_chart_fetcher';
import { useTransactionThroughputChartsFetcher } from '../../../../hooks/use_transaction_throughput_chart_fetcher';
import { LatencyChart } from '../latency_chart';
import { TimeseriesChart } from '../timeseries_chart';
import { TransactionBreakdownChart } from '../transaction_breakdown_chart';
import { TransactionErrorRateChart } from '../transaction_error_rate_chart/';
import { getResponseTimeTickFormatter } from './helper';
import { MLHeader } from './ml_header';
import { useFormatter } from './use_formatter';
export function TransactionCharts() {
const { urlParams } = useUrlParams();
const { transactionType } = urlParams;
const {
latencyChartsData,
latencyChartsStatus,
} = useTransactionLatencyChartsFetcher();
const {
throughputChartsData,
throughputChartsStatus,
} = useTransactionThroughputChartsFetcher();
const { latencyTimeseries, anomalyTimeseries, mlJobId } = latencyChartsData;
const { throughputTimeseries } = throughputChartsData;
const { formatter, toggleSerie } = useFormatter(latencyTimeseries);
return (
<>
<AnnotationsContextProvider>
@ -59,35 +42,7 @@ export function TransactionCharts() {
<EuiFlexGrid columns={2} gutterSize="s">
<EuiFlexItem data-cy={`transaction-duration-charts`}>
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="xs">
<span>{responseTimeLabel(transactionType)}</span>
</EuiTitle>
</EuiFlexItem>
<LicenseContext.Consumer>
{(license) => (
<MLHeader
hasValidMlLicense={
license?.getFeature('ml').isAvailable
}
mlJobId={mlJobId}
/>
)}
</LicenseContext.Consumer>
</EuiFlexGroup>
<TimeseriesChart
fetchStatus={latencyChartsStatus}
id="transactionDuration"
timeseries={latencyTimeseries}
yLabelFormat={getResponseTimeTickFormatter(formatter)}
anomalySeries={anomalyTimeseries}
onToggleLegend={(serie) => {
if (serie) {
toggleSerie(serie);
}
}}
/>
<LatencyChart />
</EuiPanel>
</EuiFlexItem>
@ -137,29 +92,3 @@ function tpmLabel(type?: string) {
}
);
}
function responseTimeLabel(type?: string) {
switch (type) {
case TRANSACTION_PAGE_LOAD:
return i18n.translate(
'xpack.apm.metrics.transactionChart.pageLoadTimesLabel',
{
defaultMessage: 'Page load times',
}
);
case TRANSACTION_ROUTE_CHANGE:
return i18n.translate(
'xpack.apm.metrics.transactionChart.routeChangeTimesLabel',
{
defaultMessage: 'Route change times',
}
);
default:
return i18n.translate(
'xpack.apm.metrics.transactionChart.transactionDurationLabel',
{
defaultMessage: 'Transaction duration',
}
);
}
}

View file

@ -1,83 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { SeriesIdentifier } from '@elastic/charts';
import { renderHook } from '@testing-library/react-hooks';
import { act } from 'react-test-renderer';
import { toMicroseconds } from '../../../../../common/utils/formatters';
import { TimeSeries } from '../../../../../typings/timeseries';
import { useFormatter } from './use_formatter';
describe('useFormatter', () => {
const timeSeries = ([
{
title: 'avg',
data: [
{ x: 1, y: toMicroseconds(11, 'minutes') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
{ x: 3, y: toMicroseconds(60, 'seconds') },
],
},
{
title: '95th percentile',
data: [
{ x: 1, y: toMicroseconds(120, 'seconds') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
{ x: 3, y: toMicroseconds(60, 'seconds') },
],
},
{
title: '99th percentile',
data: [
{ x: 1, y: toMicroseconds(60, 'seconds') },
{ x: 2, y: toMicroseconds(5, 'minutes') },
{ x: 3, y: toMicroseconds(100, 'seconds') },
],
},
] as unknown) as TimeSeries[];
it('returns new formatter when disabled series state changes', () => {
const { result } = renderHook(() => useFormatter(timeSeries));
expect(
result.current.formatter(toMicroseconds(120, 'seconds')).formatted
).toEqual('2.0 min');
act(() => {
result.current.toggleSerie({
specId: 'avg',
} as SeriesIdentifier);
});
expect(
result.current.formatter(toMicroseconds(120, 'seconds')).formatted
).toEqual('120 s');
});
it('falls back to the first formatter when disabled series is empty', () => {
const { result } = renderHook(() => useFormatter(timeSeries));
expect(
result.current.formatter(toMicroseconds(120, 'seconds')).formatted
).toEqual('2.0 min');
act(() => {
result.current.toggleSerie({
specId: 'avg',
} as SeriesIdentifier);
});
expect(
result.current.formatter(toMicroseconds(120, 'seconds')).formatted
).toEqual('120 s');
act(() => {
result.current.toggleSerie({
specId: 'avg',
} as SeriesIdentifier);
});
expect(
result.current.formatter(toMicroseconds(120, 'seconds')).formatted
).toEqual('2.0 min');
});
});

View file

@ -1,50 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { SeriesIdentifier } from '@elastic/charts';
import { omit } from 'lodash';
import { useState } from 'react';
import {
getDurationFormatter,
TimeFormatter,
} from '../../../../../common/utils/formatters';
import { TimeSeries } from '../../../../../typings/timeseries';
import { getMaxY } from './helper';
export const useFormatter = (
series?: TimeSeries[]
): {
formatter: TimeFormatter;
toggleSerie: (disabledSerie: SeriesIdentifier) => void;
} => {
const [disabledSeries, setDisabledSeries] = useState<
Record<SeriesIdentifier['specId'], 0>
>({});
const visibleSeries = series?.filter(
(serie) => disabledSeries[serie.title] === undefined
);
const maxY = getMaxY(visibleSeries || series || []);
const formatter = getDurationFormatter(maxY);
const toggleSerie = ({ specId }: SeriesIdentifier) => {
if (disabledSeries[specId] !== undefined) {
setDisabledSeries((prevState) => {
return omit(prevState, specId);
});
} else {
setDisabledSeries((prevState) => {
return { ...prevState, [specId]: 0 };
});
}
};
return {
formatter,
toggleSerie,
};
};

View file

@ -5,19 +5,19 @@
*/
import { Location } from 'history';
import { IUrlParams } from './types';
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 {
removeUndefinedProps,
getStart,
getEnd,
getStart,
removeUndefinedProps,
toBoolean,
toNumber,
toString,
} from './helpers';
import { toQuery } from '../../components/shared/Links/url_helpers';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config';
import { pickKeys } from '../../../common/utils/pick_keys';
import { IUrlParams } from './types';
type TimeUrlParams = Pick<
IUrlParams,
@ -48,6 +48,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) {
environment,
searchTerm,
percentile,
latencyAggregationType,
} = query;
const localUIFilters = pickKeys(query, ...localUIFilterNames);
@ -77,6 +78,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) {
transactionType,
searchTerm: toString(searchTerm),
percentile: toNumber(percentile),
latencyAggregationType,
// ui filters
environment,

View file

@ -28,4 +28,5 @@ export type IUrlParams = {
pageSize?: number;
searchTerm?: string;
percentile?: number;
latencyAggregationType?: string;
} & Partial<Record<LocalUIFilterName, string>>;

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LatencyAggregationType } from '../../common/latency_aggregation_types';
import { UIFilters } from '../../typings/ui_filters';
import { IUrlParams } from '../context/url_params_context/types';
import * as urlParams from '../context/url_params_context/use_url_params';
import { useLatencyAggregationType } from './use_latency_Aggregation_type';
describe('useLatencyAggregationType', () => {
afterAll(() => {
jest.clearAllMocks();
});
it('returns avg when no value was given', () => {
jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({
urlParams: { latencyAggregationType: undefined } as IUrlParams,
refreshTimeRange: jest.fn(),
uiFilters: {} as UIFilters,
});
const latencyAggregationType = useLatencyAggregationType();
expect(latencyAggregationType).toEqual(LatencyAggregationType.avg);
});
it('returns avg when no value does not match any of the availabe options', () => {
jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({
urlParams: { latencyAggregationType: 'invalid_type' } as IUrlParams,
refreshTimeRange: jest.fn(),
uiFilters: {} as UIFilters,
});
const latencyAggregationType = useLatencyAggregationType();
expect(latencyAggregationType).toEqual(LatencyAggregationType.avg);
});
it('returns the value in the url', () => {
jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({
urlParams: { latencyAggregationType: 'p95' } as IUrlParams,
refreshTimeRange: jest.fn(),
uiFilters: {} as UIFilters,
});
const latencyAggregationType = useLatencyAggregationType();
expect(latencyAggregationType).toEqual(LatencyAggregationType.p95);
});
});

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;
* you may not use this file except in compliance with the Elastic License.
*/
import { LatencyAggregationType } from '../../common/latency_aggregation_types';
import { useUrlParams } from '../context/url_params_context/use_url_params';
export function useLatencyAggregationType(): LatencyAggregationType {
const {
urlParams: { latencyAggregationType },
} = useUrlParams();
if (!latencyAggregationType) {
return LatencyAggregationType.avg;
}
if (latencyAggregationType in LatencyAggregationType) {
return latencyAggregationType as LatencyAggregationType;
}
return LatencyAggregationType.avg;
}

View file

@ -8,20 +8,30 @@ import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useFetcher } from './use_fetcher';
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 { useLatencyAggregationType } from './use_latency_Aggregation_type';
export function useTransactionLatencyChartsFetcher() {
const { serviceName } = useParams<{ serviceName?: string }>();
const { transactionType } = useApmServiceContext();
const latencyAggregationType = useLatencyAggregationType();
const theme = useTheme();
const {
urlParams: { transactionType, start, end, transactionName },
urlParams: { start, end, transactionName },
uiFilters,
} = useUrlParams();
const { data, error, status } = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
if (
serviceName &&
start &&
end &&
transactionType &&
latencyAggregationType
) {
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/latency',
@ -33,17 +43,33 @@ export function useTransactionLatencyChartsFetcher() {
transactionType,
transactionName,
uiFilters: JSON.stringify(uiFilters),
latencyAggregationType,
},
},
});
}
},
[serviceName, start, end, transactionName, transactionType, uiFilters]
[
serviceName,
start,
end,
transactionName,
transactionType,
uiFilters,
latencyAggregationType,
]
);
const memoizedData = useMemo(
() => getLatencyChartSelector({ latencyChart: data, theme }),
[data, theme]
() =>
getLatencyChartSelector({
latencyChart: data,
theme,
latencyAggregationType,
}),
// It should only update when the data has changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
);
return {

View file

@ -5,6 +5,7 @@
*/
import { EuiTheme } from '../../../xpack_legacy/common';
import { LatencyAggregationType } from '../../common/latency_aggregation_types';
import {
getLatencyChartSelector,
LatencyChartsResponse,
@ -21,11 +22,7 @@ const theme = {
const latencyChartData = {
overallAvgDuration: 1,
latencyTimeseries: {
avg: [{ x: 1, y: 10 }],
p95: [{ x: 2, y: 5 }],
p99: [{ x: 3, y: 8 }],
},
latencyTimeseries: [{ x: 1, y: 10 }],
anomalyTimeseries: {
jobId: '1',
anomalyBoundaries: [{ x: 1, y: 2 }],
@ -43,32 +40,60 @@ describe('getLatencyChartSelector', () => {
anomalyTimeseries: undefined,
});
});
it('returns latency time series', () => {
it('returns average timeseries', () => {
const { anomalyTimeseries, ...latencyWithouAnomaly } = latencyChartData;
const latencyTimeseries = getLatencyChartSelector({
latencyChart: latencyWithouAnomaly as LatencyChartsResponse,
theme,
latencyAggregationType: LatencyAggregationType.avg,
});
expect(latencyTimeseries).toEqual({
latencyTimeseries: [
{
title: 'Avg.',
title: 'Average',
data: [{ x: 1, y: 10 }],
legendValue: '1 μs',
type: 'linemark',
color: 'blue',
},
],
});
});
it('returns 95th percentile timeseries', () => {
const { anomalyTimeseries, ...latencyWithouAnomaly } = latencyChartData;
const latencyTimeseries = getLatencyChartSelector({
latencyChart: latencyWithouAnomaly as LatencyChartsResponse,
theme,
latencyAggregationType: LatencyAggregationType.p95,
});
expect(latencyTimeseries).toEqual({
latencyTimeseries: [
{
title: '95th percentile',
data: [{ x: 1, y: 10 }],
titleShort: '95th',
data: [{ x: 2, y: 5 }],
type: 'linemark',
color: 'red',
},
],
});
});
it('returns 99th percentile timeseries', () => {
const { anomalyTimeseries, ...latencyWithouAnomaly } = latencyChartData;
const latencyTimeseries = getLatencyChartSelector({
latencyChart: latencyWithouAnomaly as LatencyChartsResponse,
theme,
latencyAggregationType: LatencyAggregationType.p99,
});
expect(latencyTimeseries).toEqual({
latencyTimeseries: [
{
title: '99th percentile',
data: [{ x: 1, y: 10 }],
titleShort: '99th',
data: [{ x: 3, y: 8 }],
type: 'linemark',
color: 'black',
},
@ -82,27 +107,14 @@ describe('getLatencyChartSelector', () => {
const latencyTimeseries = getLatencyChartSelector({
latencyChart: latencyChartData,
theme,
latencyAggregationType: LatencyAggregationType.p99,
});
expect(latencyTimeseries).toEqual({
latencyTimeseries: [
{
title: 'Avg.',
data: [{ x: 1, y: 10 }],
legendValue: '1 μs',
type: 'linemark',
color: 'blue',
},
{
title: '95th percentile',
titleShort: '95th',
data: [{ x: 2, y: 5 }],
type: 'linemark',
color: 'red',
},
{
title: '99th percentile',
titleShort: '99th',
data: [{ x: 3, y: 8 }],
data: [{ x: 1, y: 10 }],
type: 'linemark',
color: 'black',
},

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { rgba } from 'polished';
import { EuiTheme } from '../../../observability/public';
import { LatencyAggregationType } from '../../common/latency_aggregation_types';
import { asDuration } from '../../common/utils/formatters';
import {
Coordinate,
@ -17,7 +18,7 @@ import { APIReturnType } from '../services/rest/createCallApmApi';
export type LatencyChartsResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/latency'>;
interface LatencyChart {
latencyTimeseries: TimeSeries[];
latencyTimeseries: Array<TimeSeries<Coordinate>>;
mlJobId?: string;
anomalyTimeseries?: {
bounderies: TimeSeries;
@ -28,11 +29,13 @@ interface LatencyChart {
export function getLatencyChartSelector({
latencyChart,
theme,
latencyAggregationType,
}: {
latencyChart?: LatencyChartsResponse;
theme: EuiTheme;
latencyAggregationType?: LatencyAggregationType;
}): LatencyChart {
if (!latencyChart) {
if (!latencyChart?.latencyTimeseries || !latencyAggregationType) {
return {
latencyTimeseries: [],
mlJobId: undefined,
@ -40,7 +43,11 @@ export function getLatencyChartSelector({
};
}
return {
latencyTimeseries: getLatencyTimeseries({ latencyChart, theme }),
latencyTimeseries: getLatencyTimeseries({
latencyChart,
theme,
latencyAggregationType,
}),
mlJobId: latencyChart.anomalyTimeseries?.jobId,
anomalyTimeseries: getAnnomalyTimeseries({
anomalyTimeseries: latencyChart.anomalyTimeseries,
@ -52,53 +59,60 @@ export function getLatencyChartSelector({
function getLatencyTimeseries({
latencyChart,
theme,
latencyAggregationType,
}: {
latencyChart: LatencyChartsResponse;
theme: EuiTheme;
latencyAggregationType: LatencyAggregationType;
}) {
const { overallAvgDuration } = latencyChart;
const { avg, p95, p99 } = latencyChart.latencyTimeseries;
const { latencyTimeseries } = latencyChart;
const series = [
{
title: i18n.translate(
'xpack.apm.transactions.latency.chart.averageLabel',
switch (latencyAggregationType) {
case 'avg': {
return [
{
defaultMessage: 'Avg.',
}
),
data: avg,
legendValue: asDuration(overallAvgDuration),
type: 'linemark',
color: theme.eui.euiColorVis1,
},
{
title: i18n.translate(
'xpack.apm.transactions.latency.chart.95thPercentileLabel',
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 [
{
defaultMessage: '95th percentile',
}
),
titleShort: '95th',
data: p95,
type: 'linemark',
color: theme.eui.euiColorVis5,
},
{
title: i18n.translate(
'xpack.apm.transactions.latency.chart.99thPercentileLabel',
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 [
{
defaultMessage: '99th percentile',
}
),
titleShort: '99th',
data: p99,
type: 'linemark',
color: theme.eui.euiColorVis7,
},
];
return series;
title: i18n.translate(
'xpack.apm.transactions.latency.chart.99thPercentileLabel',
{ defaultMessage: '99th percentile' }
),
titleShort: '99th',
data: latencyTimeseries,
type: 'linemark',
color: theme.eui.euiColorVis7,
},
];
}
}
return [];
}
function getAnnomalyTimeseries({

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
export function getLatencyAggregation(
latencyAggregationType: LatencyAggregationType,
field: string
) {
return {
latency: {
...(latencyAggregationType === LatencyAggregationType.avg
? { avg: { field } }
: {
percentiles: {
field,
percents: [
latencyAggregationType === LatencyAggregationType.p95 ? 95 : 99,
],
},
}),
},
};
}
export function getLatencyValue({
latencyAggregationType,
aggregation,
}: {
latencyAggregationType: LatencyAggregationType;
aggregation:
| { value: number | null }
| { values: Record<string, number | null> };
}) {
if ('value' in aggregation) {
return aggregation.value;
}
if ('values' in aggregation) {
if (latencyAggregationType === LatencyAggregationType.p95) {
return aggregation.values['95.0'];
}
if (latencyAggregationType === LatencyAggregationType.p99) {
return aggregation.values['99.0'];
}
}
return null;
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { PromiseReturnType } from '../../../../../observability/typings/common';
import { EventOutcome } from '../../../../common/event_outcome';
import { rangeFilter } from '../../../../common/utils/range_filter';
@ -21,6 +22,7 @@ import {
} from '../../helpers/aggregated_transactions';
import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { getLatencyAggregation } from '../../helpers/latency_aggregation_type';
export type TransactionGroupTimeseriesData = PromiseReturnType<
typeof getTimeseriesDataForTransactionGroups
@ -36,6 +38,7 @@ export async function getTimeseriesDataForTransactionGroups({
searchAggregatedTransactions,
size,
numBuckets,
latencyAggregationType,
}: {
apmEventClient: APMEventClient;
start: number;
@ -46,9 +49,14 @@ export async function getTimeseriesDataForTransactionGroups({
searchAggregatedTransactions: boolean;
size: number;
numBuckets: number;
latencyAggregationType: LatencyAggregationType;
}) {
const { intervalString } = getBucketSize({ start, end, numBuckets });
const field = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
);
const timeseriesResponse = await apmEventClient.search({
apm: {
events: [
@ -92,35 +100,11 @@ export async function getTimeseriesDataForTransactionGroups({
},
},
aggs: {
avg_latency: {
avg: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
transaction_count: {
value_count: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
...getLatencyAggregation(latencyAggregationType, field),
transaction_count: { value_count: { field } },
[EVENT_OUTCOME]: {
filter: {
term: {
[EVENT_OUTCOME]: EventOutcome.failure,
},
},
aggs: {
transaction_count: {
value_count: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
},
filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
aggs: { transaction_count: { value_count: { field } } },
},
},
},

View file

@ -5,6 +5,7 @@
*/
import { orderBy } from 'lodash';
import { ValuesType } from 'utility-types';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { PromiseReturnType } from '../../../../../observability/typings/common';
import { EventOutcome } from '../../../../common/event_outcome';
import { ESFilter } from '../../../../../../typings/elasticsearch';
@ -19,6 +20,10 @@ import {
getTransactionDurationFieldForAggregatedTransactions,
} from '../../helpers/aggregated_transactions';
import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client';
import {
getLatencyAggregation,
getLatencyValue,
} from '../../helpers/latency_aggregation_type';
export type ServiceOverviewTransactionGroupSortField =
| 'latency'
@ -41,6 +46,7 @@ export async function getTransactionGroupsForPage({
sortDirection,
pageIndex,
size,
latencyAggregationType,
}: {
apmEventClient: APMEventClient;
searchAggregatedTransactions: boolean;
@ -52,7 +58,12 @@ export async function getTransactionGroupsForPage({
sortDirection: 'asc' | 'desc';
pageIndex: number;
size: number;
latencyAggregationType: LatencyAggregationType;
}) {
const field = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
);
const response = await apmEventClient.search({
apm: {
events: [
@ -77,40 +88,14 @@ export async function getTransactionGroupsForPage({
terms: {
field: TRANSACTION_NAME,
size: 500,
order: {
_count: 'desc',
},
order: { _count: 'desc' },
},
aggs: {
avg_latency: {
avg: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
transaction_count: {
value_count: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
...getLatencyAggregation(latencyAggregationType, field),
transaction_count: { value_count: { field } },
[EVENT_OUTCOME]: {
filter: {
term: {
[EVENT_OUTCOME]: EventOutcome.failure,
},
},
aggs: {
transaction_count: {
value_count: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
},
filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
aggs: { transaction_count: { value_count: { field } } },
},
},
},
@ -128,7 +113,10 @@ export async function getTransactionGroupsForPage({
return {
name: bucket.key as string,
latency: bucket.avg_latency.value,
latency: getLatencyValue({
latencyAggregationType,
aggregation: bucket.latency,
}),
throughput: bucket.transaction_count.value,
errorRate,
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { getTimeseriesDataForTransactionGroups } from './get_timeseries_data_for_transaction_groups';
import {
@ -21,6 +22,7 @@ export async function getServiceTransactionGroups({
sortDirection,
sortField,
searchAggregatedTransactions,
latencyAggregationType,
}: {
serviceName: string;
setup: Setup & SetupTimeRange;
@ -30,6 +32,7 @@ export async function getServiceTransactionGroups({
sortDirection: 'asc' | 'desc';
sortField: ServiceOverviewTransactionGroupSortField;
searchAggregatedTransactions: boolean;
latencyAggregationType: LatencyAggregationType;
}) {
const { apmEventClient, start, end, esFilter } = setup;
@ -48,6 +51,7 @@ export async function getServiceTransactionGroups({
sortDirection,
size,
searchAggregatedTransactions,
latencyAggregationType,
});
const transactionNames = transactionGroups.map((group) => group.name);
@ -62,6 +66,7 @@ export async function getServiceTransactionGroups({
serviceName,
size,
transactionNames,
latencyAggregationType,
});
return {
@ -70,6 +75,7 @@ export async function getServiceTransactionGroups({
timeseriesData,
start,
end,
latencyAggregationType,
}),
totalTransactionGroups,
isAggregationAccurate,

View file

@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
} from '../../../../common/transaction_types';
import { getLatencyValue } from '../../helpers/latency_aggregation_type';
import { TransactionGroupTimeseriesData } from './get_timeseries_data_for_transaction_groups';
@ -20,11 +22,13 @@ export function mergeTransactionGroupData({
end,
transactionGroups,
timeseriesData,
latencyAggregationType,
}: {
start: number;
end: number;
transactionGroups: TransactionGroupWithoutTimeseriesData[];
timeseriesData: TransactionGroupTimeseriesData;
latencyAggregationType: LatencyAggregationType;
}) {
const deltaAsMinutes = (end - start) / 1000 / 60;
@ -53,7 +57,10 @@ export function mergeTransactionGroupData({
...acc.latency,
timeseries: acc.latency.timeseries.concat({
x: point.key,
y: point.avg_latency.value,
y: getLatencyValue({
latencyAggregationType,
aggregation: point.latency,
}),
}),
},
throughput: {

View file

@ -32,7 +32,7 @@ export async function getAnomalySeries({
setup: Setup & SetupTimeRange;
logger: Logger;
}) {
const timeseriesDates = latencyTimeseries?.avg?.map(({ x }) => x);
const timeseriesDates = latencyTimeseries?.map(({ x }) => x);
/*
* don't fetch:

View file

@ -10,6 +10,7 @@ import {
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { rangeFilter } from '../../../../common/utils/range_filter';
import {
getDocumentTypeFilterForAggregatedTransactions,
@ -18,8 +19,10 @@ import {
} from '../../../lib/helpers/aggregated_transactions';
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request';
import { convertLatencyBucketsToCoordinates } from './transform';
import {
getLatencyAggregation,
getLatencyValue,
} from '../../helpers/latency_aggregation_type';
export type LatencyChartsSearchResponse = PromiseReturnType<
typeof searchLatency
>;
@ -30,12 +33,14 @@ async function searchLatency({
transactionName,
setup,
searchAggregatedTransactions,
latencyAggregationType,
}: {
serviceName: string;
transactionType: string | undefined;
transactionName: string | undefined;
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
latencyAggregationType: LatencyAggregationType;
}) {
const { start, end, apmEventClient } = setup;
const { intervalString } = getBucketSize({ start, end });
@ -57,7 +62,7 @@ async function searchLatency({
filter.push({ term: { [TRANSACTION_TYPE]: transactionType } });
}
const field = getTransactionDurationFieldForAggregatedTransactions(
const transactionDurationField = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
);
@ -80,18 +85,12 @@ async function searchLatency({
min_doc_count: 0,
extended_bounds: { min: start, max: end },
},
aggs: {
avg: { avg: { field } },
pct: {
percentiles: {
field,
percents: [95, 99],
hdr: { number_of_significant_value_digits: 2 },
},
},
},
aggs: getLatencyAggregation(
latencyAggregationType,
transactionDurationField
),
},
overall_avg_duration: { avg: { field } },
overall_avg_duration: { avg: { field: transactionDurationField } },
},
},
};
@ -105,12 +104,14 @@ export async function getLatencyTimeseries({
transactionName,
setup,
searchAggregatedTransactions,
latencyAggregationType,
}: {
serviceName: string;
transactionType: string | undefined;
transactionName: string | undefined;
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
latencyAggregationType: LatencyAggregationType;
}) {
const response = await searchLatency({
serviceName,
@ -118,20 +119,26 @@ export async function getLatencyTimeseries({
transactionName,
setup,
searchAggregatedTransactions,
latencyAggregationType,
});
if (!response.aggregations) {
return {
latencyTimeseries: { avg: [], p95: [], p99: [] },
overallAvgDuration: null,
};
return { latencyTimeseries: [], overallAvgDuration: null };
}
return {
overallAvgDuration:
response.aggregations.overall_avg_duration.value || null,
latencyTimeseries: convertLatencyBucketsToCoordinates(
response.aggregations.latencyTimeseries.buckets
latencyTimeseries: response.aggregations.latencyTimeseries.buckets.map(
(bucket) => {
return {
x: bucket.key,
y: getLatencyValue({
latencyAggregationType,
aggregation: bucket.latency,
}),
};
}
),
};
}

View file

@ -1,31 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { isNumber } from 'lodash';
import { LatencyChartsSearchResponse } from '.';
import { Coordinate } from '../../../../typings/timeseries';
type LatencyBuckets = Required<LatencyChartsSearchResponse>['aggregations']['latencyTimeseries']['buckets'];
export function convertLatencyBucketsToCoordinates(
latencyBuckets: LatencyBuckets = []
) {
return latencyBuckets.reduce(
(acc, bucket) => {
const { '95.0': p95, '99.0': p99 } = bucket.pct.values;
acc.avg.push({ x: bucket.key, y: bucket.avg.value });
acc.p95.push({ x: bucket.key, y: isNumber(p95) ? p95 : null });
acc.p99.push({ x: bucket.key, y: isNumber(p99) ? p99 : null });
return acc;
},
{
avg: [] as Coordinate[],
p95: [] as Coordinate[],
p99: [] as Coordinate[],
}
);
}

View file

@ -19,6 +19,10 @@ import { getTransactionGroupList } from '../lib/transaction_groups';
import { getErrorRate } from '../lib/transaction_groups/get_error_rate';
import { getLatencyTimeseries } from '../lib/transactions/get_latency_charts';
import { getThroughputCharts } from '../lib/transactions/get_throughput_charts';
import {
LatencyAggregationType,
latencyAggregationTypeRt,
} from '../../common/latency_aggregation_types';
/**
* Returns a list of transactions grouped by name
@ -78,6 +82,7 @@ export const transactionGroupsOverviewRoute = createRoute({
t.literal('errorRate'),
t.literal('impact'),
]),
latencyAggregationType: latencyAggregationTypeRt,
}),
]),
}),
@ -93,7 +98,14 @@ export const transactionGroupsOverviewRoute = createRoute({
const {
path: { serviceName },
query: { size, numBuckets, pageIndex, sortDirection, sortField },
query: {
size,
numBuckets,
pageIndex,
sortDirection,
sortField,
latencyAggregationType,
},
} = context.params;
return getServiceTransactionGroups({
@ -105,6 +117,7 @@ export const transactionGroupsOverviewRoute = createRoute({
sortDirection,
sortField,
numBuckets,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
});
},
});
@ -117,9 +130,12 @@ export const transactionLatencyChatsRoute = createRoute({
}),
query: t.intersection([
t.partial({
transactionType: t.string,
transactionName: t.string,
}),
t.type({
transactionType: t.string,
latencyAggregationType: latencyAggregationTypeRt,
}),
uiFiltersRt,
rangeRt,
]),
@ -129,7 +145,11 @@ export const transactionLatencyChatsRoute = createRoute({
const setup = await setupRequest(context, request);
const logger = context.logger;
const { serviceName } = context.params.path;
const { transactionType, transactionName } = context.params.query;
const {
transactionType,
transactionName,
latencyAggregationType,
} = context.params.query;
if (!setup.uiFilters.environment) {
throw Boom.badRequest(
@ -152,7 +172,10 @@ export const transactionLatencyChatsRoute = createRoute({
const {
latencyTimeseries,
overallAvgDuration,
} = await getLatencyTimeseries(options);
} = await getLatencyTimeseries({
...options,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
});
const anomalyTimeseries = await getAnomalySeries({
...options,

View file

@ -4930,10 +4930,7 @@
"xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:",
"xpack.apm.metrics.transactionChart.machineLearningTooltip": "平均期間の周りのストリームには予測バウンドが表示されます。異常スコアが>= 75の場合、注釈が表示されます。",
"xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません",
"xpack.apm.metrics.transactionChart.pageLoadTimesLabel": "ページ読み込み時間",
"xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "1 分あたりのリクエスト",
"xpack.apm.metrics.transactionChart.routeChangeTimesLabel": "ルート変更時間",
"xpack.apm.metrics.transactionChart.transactionDurationLabel": "トランザクション時間",
"xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "1 分あたりのトランザクション数",
"xpack.apm.metrics.transactionChart.viewJob": "ジョブを表示:",
"xpack.apm.notAvailableLabel": "N/A",

View file

@ -4933,10 +4933,7 @@
"xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning",
"xpack.apm.metrics.transactionChart.machineLearningTooltip": "环绕平均持续时间的流显示预期边界。对 ≥ 75 的异常分数显示标注。",
"xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "使用搜索栏筛选时Machine Learning 结果处于隐藏状态",
"xpack.apm.metrics.transactionChart.pageLoadTimesLabel": "页面加载时间",
"xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "每分钟请求数",
"xpack.apm.metrics.transactionChart.routeChangeTimesLabel": "路由更改时间",
"xpack.apm.metrics.transactionChart.transactionDurationLabel": "事务持续时间",
"xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "每分钟事务数",
"xpack.apm.metrics.transactionChart.viewJob": "查看作业:",
"xpack.apm.notAvailableLabel": "不适用",

View file

@ -107,21 +107,14 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
},
{
req: {
url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&transactionType=bar&uiFilters=%7B%22environment%22%3A%22testing%22%7D`,
url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&transactionType=bar&latencyAggregationType=avg&uiFilters=%7B%22environment%22%3A%22testing%22%7D`,
},
expectForbidden: expect403,
expectResponse: expect200,
},
{
req: {
url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&uiFilters=%7B%22environment%22%3A%22testing%22%7D`,
},
expectForbidden: expect403,
expectResponse: expect200,
},
{
req: {
url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&transactionType=bar&transactionName=baz&uiFilters=%7B%22environment%22%3A%22testing%22%7D`,
url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&transactionType=bar&latencyAggregationType=avg&transactionName=baz&uiFilters=%7B%22environment%22%3A%22testing%22%7D`,
},
expectForbidden: expect403,
expectResponse: expect200,

View file

@ -22,17 +22,31 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('Latency', () => {
describe('when data is not loaded ', () => {
it('returns 400 when latencyAggregationType is not informed', async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request`
);
expect(response.status).to.be(400);
});
it('returns 400 when transactionType is not informed', async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&latencyAggregationType=avg`
);
expect(response.status).to.be(400);
});
it('handles the empty state', async () => {
const response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}`
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&latencyAggregationType=avg&transactionType=request`
);
expect(response.status).to.be(200);
expect(response.body.overallAvgDuration).to.be(null);
expect(response.body.latencyTimeseries.avg.length).to.be(0);
expect(response.body.latencyTimeseries.p95.length).to.be(0);
expect(response.body.latencyTimeseries.p99.length).to.be(0);
expect(response.body.latencyTimeseries.length).to.be(0);
});
});
@ -42,19 +56,46 @@ export default function ApiTest({ getService }: FtrProviderContext) {
let response: PromiseReturnType<typeof supertest.get>;
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}`
);
describe('average latency type', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=avg`
);
});
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);
});
});
it('returns average duration and timeseries', async () => {
expect(response.status).to.be(200);
describe('95th percentile latency type', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=p95`
);
});
expect(response.body.overallAvgDuration).not.to.be(null);
expect(response.body.latencyTimeseries.avg.length).to.be.greaterThan(0);
expect(response.body.latencyTimeseries.p95.length).to.be.greaterThan(0);
expect(response.body.latencyTimeseries.p99.length).to.be.greaterThan(0);
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);
});
});
describe('99th percentile latency type', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=request&latencyAggregationType=p99`
);
});
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);
});
});
});
});

View file

@ -32,6 +32,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
latencyAggregationType: 'avg',
},
})
);
@ -62,6 +63,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
latencyAggregationType: 'avg',
},
})
);
@ -138,6 +140,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
latencyAggregationType: 'avg',
},
})
);
@ -162,6 +165,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
latencyAggregationType: 'avg',
},
})
);
@ -186,6 +190,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
pageIndex: 0,
sortDirection: 'desc',
sortField: 'latency',
latencyAggregationType: 'avg',
},
})
);
@ -212,6 +217,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
latencyAggregationType: 'avg',
},
})
);
@ -239,6 +245,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
pageIndex,
sortDirection: 'desc',
sortField: 'impact',
latencyAggregationType: 'avg',
},
})
);

View file

@ -34,7 +34,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const uiFilters = encodeURIComponent(JSON.stringify({}));
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}`
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg`
);
});
it('should return an error response', () => {
@ -45,7 +45,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('without uiFilters', () => {
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}`
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&latencyAggregationType=avg`
);
});
it('should return an error response', () => {
@ -57,7 +57,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'production' }));
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}`
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg`
);
});
@ -86,7 +86,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-python/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}`
`/api/apm/services/opbeans-python/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg`
);
});
@ -112,7 +112,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'ENVIRONMENT_ALL' }));
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}`
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg`
);
});
@ -131,7 +131,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
);
before(async () => {
response = await supertest.get(
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}`
`/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}&latencyAggregationType=avg`
);
});