mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
0dfcbe92ed
commit
542a8aa1d4
35 changed files with 716 additions and 555 deletions
18
x-pack/plugins/apm/common/latency_aggregation_types.ts
Normal file
18
x-pack/plugins/apm/common/latency_aggregation_types.ts
Normal 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'),
|
||||
]);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -15,6 +15,7 @@ const persistedFilters: Array<keyof APMQueryParams> = [
|
|||
'containerId',
|
||||
'podName',
|
||||
'serviceVersion',
|
||||
'latencyAggregationType',
|
||||
];
|
||||
|
||||
export function useTransactionOverviewHref(serviceName: string) {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -28,4 +28,5 @@ export type IUrlParams = {
|
|||
pageSize?: number;
|
||||
searchTerm?: string;
|
||||
percentile?: number;
|
||||
latencyAggregationType?: string;
|
||||
} & Partial<Record<LocalUIFilterName, string>>;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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[],
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "不适用",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue