[APM] Break down transaction table api removing the sparklines (#88946)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Dario Gieselaar <dario.gieselaar@elastic.co>
This commit is contained in:
Cauê Marcondes 2021-02-12 11:57:01 +01:00 committed by GitHub
parent 6406e99642
commit 2fcf2a91cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1426 additions and 914 deletions

View file

@ -80,21 +80,21 @@ describe('ServiceOverview', () => {
status: FETCH_STATUS.SUCCESS,
});
/* eslint-disable @typescript-eslint/naming-convention */
const calls = {
// eslint-disable-next-line @typescript-eslint/naming-convention
'GET /api/apm/services/{serviceName}/error_groups': {
error_groups: [],
total_error_groups: 0,
},
'GET /api/apm/services/{serviceName}/transactions/groups/overview': {
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': {
transactionGroups: [],
totalTransactionGroups: 0,
isAggregationAccurate: true,
},
'GET /api/apm/services/{serviceName}/dependencies': [],
// eslint-disable-next-line @typescript-eslint/naming-convention
'GET /api/apm/services/{serviceName}/service_overview_instances': [],
};
/* eslint-enable @typescript-eslint/naming-convention */
jest
.spyOn(callApmApiModule, 'createCallApmApi')

View file

@ -0,0 +1,163 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ValuesType } from 'utility-types';
import {
asMillisecondDuration,
asPercent,
asTransactionRate,
} from '../../../../../common/utils/formatters';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { px, unit } from '../../../../style/variables';
import { SparkPlot } from '../../../shared/charts/spark_plot';
import { ImpactBar } from '../../../shared/ImpactBar';
import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link';
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
type TransactionGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics'>;
type ServiceTransactionGroupItem = ValuesType<
TransactionGroupPrimaryStatistics['transactionGroups']
>;
type TransactionGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics'>;
function getLatencyAggregationTypeLabel(latencyAggregationType?: string) {
switch (latencyAggregationType) {
case 'avg':
return i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg',
{ defaultMessage: 'Latency (avg.)' }
);
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)' }
);
}
}
export function getColumns({
serviceName,
latencyAggregationType,
transactionGroupComparisonStatistics,
}: {
serviceName: string;
latencyAggregationType?: string;
transactionGroupComparisonStatistics?: TransactionGroupComparisonStatistics;
}): Array<EuiBasicTableColumn<ServiceTransactionGroupItem>> {
return [
{
field: 'name',
sortable: true,
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnName',
{ defaultMessage: 'Name' }
),
render: (_, { name, transactionType: type }) => {
return (
<TruncateWithTooltip
text={name}
content={
<TransactionDetailLink
serviceName={serviceName}
transactionName={name}
transactionType={type}
latencyAggregationType={latencyAggregationType}
>
{name}
</TransactionDetailLink>
}
/>
);
},
},
{
field: 'latency',
sortable: true,
name: getLatencyAggregationTypeLabel(latencyAggregationType),
width: px(unit * 10),
render: (_, { latency, name }) => {
const timeseries =
transactionGroupComparisonStatistics?.[name]?.latency;
return (
<SparkPlot
color="euiColorVis1"
compact
series={timeseries}
valueLabel={asMillisecondDuration(latency)}
/>
);
},
},
{
field: 'throughput',
sortable: true,
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnThroughput',
{ defaultMessage: 'Throughput' }
),
width: px(unit * 10),
render: (_, { throughput, name }) => {
const timeseries =
transactionGroupComparisonStatistics?.[name]?.throughput;
return (
<SparkPlot
color="euiColorVis0"
compact
series={timeseries}
valueLabel={asTransactionRate(throughput)}
/>
);
},
},
{
field: 'errorRate',
sortable: true,
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnErrorRate',
{ defaultMessage: 'Error rate' }
),
width: px(unit * 8),
render: (_, { errorRate, name }) => {
const timeseries =
transactionGroupComparisonStatistics?.[name]?.errorRate;
return (
<SparkPlot
color="euiColorVis7"
compact
series={timeseries}
valueLabel={asPercent(errorRate, 1)}
/>
);
},
},
{
field: 'impact',
sortable: true,
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnImpact',
{ defaultMessage: 'Impact' }
),
width: px(unit * 5),
render: (_, { name }) => {
const impact =
transactionGroupComparisonStatistics?.[name]?.impact ?? 0;
return <ImpactBar value={impact} size="m" />;
},
},
];
}

View file

@ -7,86 +7,41 @@
import {
EuiBasicTable,
EuiBasicTableColumn,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { orderBy } from 'lodash';
import React, { useState } from 'react';
import { ValuesType } from 'utility-types';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import {
asMillisecondDuration,
asPercent,
asTransactionRate,
} from '../../../../../common/utils/formatters';
import uuid from 'uuid';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { px, unit } from '../../../../style/variables';
import { SparkPlot } from '../../../shared/charts/spark_plot';
import { ImpactBar } from '../../../shared/ImpactBar';
import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link';
import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_link';
import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
type ServiceTransactionGroupItem = ValuesType<
APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/overview'>['transactionGroups']
>;
import { getColumns } from './get_columns';
interface Props {
serviceName: string;
}
const INITIAL_STATE = {
transactionGroups: [],
isAggregationAccurate: true,
requestId: '',
};
type SortField = 'name' | 'latency' | 'throughput' | 'errorRate' | 'impact';
type SortDirection = 'asc' | 'desc';
const PAGE_SIZE = 5;
const DEFAULT_SORT = {
direction: 'desc' as const,
field: 'impact' as const,
};
function getLatencyAggregationTypeLabel(latencyAggregationType?: string) {
switch (latencyAggregationType) {
case 'avg':
return i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg',
{
defaultMessage: 'Latency (avg.)',
}
);
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)',
}
);
}
}
export function ServiceOverviewTransactionsTable(props: Props) {
const { serviceName } = props;
const { transactionType } = useApmServiceContext();
const {
uiFilters,
urlParams: { start, end, latencyAggregationType },
} = useUrlParams();
export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
const [tableOptions, setTableOptions] = useState<{
pageIndex: number;
sort: {
@ -98,51 +53,36 @@ export function ServiceOverviewTransactionsTable(props: Props) {
sort: DEFAULT_SORT,
});
const { pageIndex, sort } = tableOptions;
const { transactionType } = useApmServiceContext();
const {
data = {
totalItemCount: 0,
items: [],
tableOptions: {
pageIndex: 0,
sort: DEFAULT_SORT,
},
},
status,
} = useFetcher(
uiFilters,
urlParams: { start, end, latencyAggregationType },
} = useUrlParams();
const { data = INITIAL_STATE, status } = useFetcher(
(callApmApi) => {
if (!start || !end || !latencyAggregationType || !transactionType) {
return;
}
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/overview',
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
size: PAGE_SIZE,
numBuckets: 20,
pageIndex: tableOptions.pageIndex,
sortField: tableOptions.sort.field,
sortDirection: tableOptions.sort.direction,
transactionType,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
latencyAggregationType,
},
},
}).then((response) => {
return {
items: response.transactionGroups,
totalItemCount: response.totalTransactionGroups,
tableOptions: {
pageIndex: tableOptions.pageIndex,
sort: {
field: tableOptions.sort.field,
direction: tableOptions.sort.direction,
},
},
requestId: uuid(),
...response,
};
});
},
@ -151,114 +91,81 @@ export function ServiceOverviewTransactionsTable(props: Props) {
start,
end,
uiFilters,
tableOptions.pageIndex,
tableOptions.sort.field,
tableOptions.sort.direction,
transactionType,
latencyAggregationType,
]
);
const {
items,
totalItemCount,
tableOptions: { pageIndex, sort },
} = data;
const { transactionGroups, requestId } = data;
const currentPageTransactionGroups = orderBy(
transactionGroups,
sort.field,
sort.direction
).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE);
const columns: Array<EuiBasicTableColumn<ServiceTransactionGroupItem>> = [
{
field: 'name',
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnName',
{
defaultMessage: 'Name',
}
),
render: (_, { name, transactionType: type }) => {
return (
<TruncateWithTooltip
text={name}
content={
<TransactionDetailLink
serviceName={serviceName}
transactionName={name}
transactionType={type}
latencyAggregationType={latencyAggregationType}
>
{name}
</TransactionDetailLink>
}
/>
);
},
const transactionNames = JSON.stringify(
currentPageTransactionGroups.map(({ name }) => name).sort()
);
const {
data: transactionGroupComparisonStatistics,
status: transactionGroupComparisonStatisticsStatus,
} = useFetcher(
(callApmApi) => {
if (
currentPageTransactionGroups.length &&
start &&
end &&
transactionType &&
latencyAggregationType
) {
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
numBuckets: 20,
transactionType,
latencyAggregationType,
transactionNames,
},
},
});
}
},
{
field: 'latency',
name: getLatencyAggregationTypeLabel(latencyAggregationType),
width: px(unit * 10),
render: (_, { latency }) => {
return (
<SparkPlot
color="euiColorVis1"
compact
series={latency.timeseries ?? undefined}
valueLabel={asMillisecondDuration(latency.value)}
/>
);
},
// only fetches statistics when requestId changes or transaction names changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[requestId, transactionNames],
{ preservePreviousData: false }
);
const columns = getColumns({
serviceName,
latencyAggregationType,
transactionGroupComparisonStatistics,
});
const isLoading =
status === FETCH_STATUS.LOADING ||
transactionGroupComparisonStatisticsStatus === FETCH_STATUS.LOADING;
const pagination = {
pageIndex,
pageSize: PAGE_SIZE,
totalItemCount: transactionGroups.length,
hidePerPageOptions: true,
};
const sorting = {
sort: {
field: sort.field,
direction: sort.direction,
},
{
field: 'throughput',
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnThroughput',
{ defaultMessage: 'Throughput' }
),
width: px(unit * 10),
render: (_, { throughput }) => {
return (
<SparkPlot
color="euiColorVis0"
compact
series={throughput.timeseries ?? undefined}
valueLabel={asTransactionRate(throughput.value)}
/>
);
},
},
{
field: 'errorRate',
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnErrorRate',
{
defaultMessage: 'Error rate',
}
),
width: px(unit * 8),
render: (_, { errorRate }) => {
return (
<SparkPlot
color="euiColorVis7"
compact
series={errorRate.timeseries ?? undefined}
valueLabel={asPercent(errorRate.value, 1)}
/>
);
},
},
{
field: 'impact',
name: i18n.translate(
'xpack.apm.serviceOverview.transactionsTableColumnImpact',
{
defaultMessage: 'Impact',
}
),
width: px(unit * 5),
render: (_, { impact }) => {
return <ImpactBar value={impact ?? 0} size="m" />;
},
},
];
};
return (
<EuiFlexGroup direction="column" gutterSize="s">
@ -295,21 +202,14 @@ export function ServiceOverviewTransactionsTable(props: Props) {
<EuiFlexItem>
<TableFetchWrapper status={status}>
<ServiceOverviewTableContainer
isEmptyAndLoading={
items.length === 0 && status === FETCH_STATUS.LOADING
}
isEmptyAndLoading={transactionGroups.length === 0 && isLoading}
>
<EuiBasicTable
loading={isLoading}
items={currentPageTransactionGroups}
columns={columns}
items={items}
pagination={{
pageIndex,
pageSize: PAGE_SIZE,
totalItemCount,
pageSizeOptions: [PAGE_SIZE],
hidePerPageOptions: true,
}}
loading={status === FETCH_STATUS.LOADING}
pagination={pagination}
sorting={sorting}
onChange={(newTableOptions: {
page?: {
index: number;
@ -326,13 +226,6 @@ export function ServiceOverviewTransactionsTable(props: Props) {
: DEFAULT_SORT,
});
}}
sorting={{
enableAllColumns: true,
sort: {
direction: sort.direction,
field: sort.field,
},
}}
/>
</ServiceOverviewTableContainer>
</TableFetchWrapper>

View file

@ -16,6 +16,7 @@ import {
Settings,
} from '@elastic/charts';
import { merge } from 'lodash';
import { Coordinate } from '../../../../../typings/timeseries';
import { useChartTheme } from '../../../../../../observability/public';
import { px, unit } from '../../../../style/variables';
import { useTheme } from '../../../../hooks/use_theme';
@ -39,7 +40,7 @@ export function SparkPlot({
compact,
}: {
color: Color;
series?: Array<{ x: number; y: number | null }> | null;
series?: Coordinate[] | null;
valueLabel: React.ReactNode;
compact?: boolean;
}) {
@ -58,18 +59,18 @@ export function SparkPlot({
const colorValue = theme.eui[color];
const chartSize = {
height: px(24),
width: compact ? px(unit * 3) : px(unit * 4),
};
return (
<EuiFlexGroup gutterSize="m" responsive={false}>
<EuiFlexItem grow={false}>
{!series || series.every((point) => point.y === null) ? (
<EuiIcon type="visLine" color="subdued" />
<EuiIcon type="visLine" color="subdued" style={chartSize} />
) : (
<Chart
size={{
height: px(24),
width: compact ? px(unit * 3) : px(unit * 4),
}}
>
<Chart size={chartSize}>
<Settings
theme={sparkplotChartTheme}
showLegend={false}

View file

@ -80,7 +80,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) {
transactionType,
searchTerm: toString(searchTerm),
percentile: toNumber(percentile),
latencyAggregationType,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
comparisonEnabled: comparisonEnabled
? toBoolean(comparisonEnabled)
: undefined,

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { LatencyAggregationType } from '../../../common/latency_aggregation_types';
import { LocalUIFilterName } from '../../../common/ui_filter';
export type IUrlParams = {
@ -29,7 +30,7 @@ export type IUrlParams = {
pageSize?: number;
searchTerm?: string;
percentile?: number;
latencyAggregationType?: string;
latencyAggregationType?: LatencyAggregationType;
comparisonEnabled?: boolean;
comparisonType?: string;
} & Partial<Record<LocalUIFilterName, string>>;

View file

@ -12,7 +12,6 @@ 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 { LatencyAggregationType } from '../../common/latency_aggregation_types';
export function useTransactionLatencyChartsFetcher() {
const { serviceName } = useParams<{ serviceName?: string }>();
@ -43,7 +42,7 @@ export function useTransactionLatencyChartsFetcher() {
transactionType,
transactionName,
uiFilters: JSON.stringify(uiFilters),
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
latencyAggregationType,
},
},
});

View file

@ -0,0 +1,167 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { keyBy } from 'lodash';
import {
EVENT_OUTCOME,
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../common/elasticsearch_fieldnames';
import { EventOutcome } from '../../../common/event_outcome';
import { LatencyAggregationType } from '../../../common/latency_aggregation_types';
import { rangeFilter } from '../../../common/utils/range_filter';
import { Coordinate } from '../../../typings/timeseries';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} from '../helpers/aggregated_transactions';
import { getBucketSize } from '../helpers/get_bucket_size';
import {
getLatencyAggregation,
getLatencyValue,
} from '../helpers/latency_aggregation_type';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate';
export async function getServiceTransactionGroupComparisonStatistics({
serviceName,
transactionNames,
setup,
numBuckets,
searchAggregatedTransactions,
transactionType,
latencyAggregationType,
}: {
serviceName: string;
transactionNames: string[];
setup: Setup & SetupTimeRange;
numBuckets: number;
searchAggregatedTransactions: boolean;
transactionType: string;
latencyAggregationType: LatencyAggregationType;
}): Promise<
Record<
string,
{
latency: Coordinate[];
throughput: Coordinate[];
errorRate: Coordinate[];
impact: number;
}
>
> {
const { apmEventClient, start, end, esFilter } = setup;
const { intervalString } = getBucketSize({ start, end, numBuckets });
const field = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
);
const response = await apmEventClient.search({
apm: {
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: transactionType } },
{ range: rangeFilter(start, end) },
...getDocumentTypeFilterForAggregatedTransactions(
searchAggregatedTransactions
),
...esFilter,
],
},
},
aggs: {
total_duration: { sum: { field } },
transaction_groups: {
terms: {
field: TRANSACTION_NAME,
include: transactionNames,
size: transactionNames.length,
},
aggs: {
transaction_group_total_duration: {
sum: { field },
},
timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval: intervalString,
min_doc_count: 0,
extended_bounds: {
min: start,
max: end,
},
},
aggs: {
throughput_rate: {
rate: {
unit: 'minute',
},
},
...getLatencyAggregation(latencyAggregationType, field),
[EVENT_OUTCOME]: {
terms: {
field: EVENT_OUTCOME,
include: [EventOutcome.failure, EventOutcome.success],
},
},
},
},
},
},
},
},
});
const buckets = response.aggregations?.transaction_groups.buckets ?? [];
const totalDuration = response.aggregations?.total_duration.value;
return keyBy(
buckets.map((bucket) => {
const transactionName = bucket.key;
const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({
x: timeseriesBucket.key,
y: getLatencyValue({
latencyAggregationType,
aggregation: timeseriesBucket.latency,
}),
}));
const throughput = bucket.timeseries.buckets.map((timeseriesBucket) => ({
x: timeseriesBucket.key,
y: timeseriesBucket.throughput_rate.value,
}));
const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({
x: timeseriesBucket.key,
y: calculateTransactionErrorPercentage(timeseriesBucket[EVENT_OUTCOME]),
}));
const transactionGroupTotalDuration =
bucket.transaction_group_total_duration.value || 0;
return {
transactionName,
latency,
throughput,
errorRate,
impact: totalDuration
? (transactionGroupTotalDuration * 100) / totalDuration
: 0,
};
}),
'transactionName'
);
}

View file

@ -5,29 +5,27 @@
* 2.0.
*/
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';
import { rangeFilter } from '../../../../common/utils/range_filter';
import {
EVENT_OUTCOME,
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
} from '../../../common/elasticsearch_fieldnames';
import { EventOutcome } from '../../../common/event_outcome';
import { LatencyAggregationType } from '../../../common/latency_aggregation_types';
import { rangeFilter } from '../../../common/utils/range_filter';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} from '../../helpers/aggregated_transactions';
import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client';
} from '../helpers/aggregated_transactions';
import { calculateThroughput } from '../helpers/calculate_throughput';
import {
getLatencyAggregation,
getLatencyValue,
} from '../../helpers/latency_aggregation_type';
import { calculateThroughput } from '../../helpers/calculate_throughput';
} from '../helpers/latency_aggregation_type';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate';
export type ServiceOverviewTransactionGroupSortField =
| 'name'
@ -36,37 +34,21 @@ export type ServiceOverviewTransactionGroupSortField =
| 'errorRate'
| 'impact';
export type TransactionGroupWithoutTimeseriesData = ValuesType<
PromiseReturnType<typeof getTransactionGroupsForPage>['transactionGroups']
>;
export async function getTransactionGroupsForPage({
apmEventClient,
searchAggregatedTransactions,
export async function getServiceTransactionGroups({
serviceName,
start,
end,
esFilter,
sortField,
sortDirection,
pageIndex,
size,
setup,
searchAggregatedTransactions,
transactionType,
latencyAggregationType,
}: {
apmEventClient: APMEventClient;
searchAggregatedTransactions: boolean;
serviceName: string;
start: number;
end: number;
esFilter: ESFilter[];
sortField: ServiceOverviewTransactionGroupSortField;
sortDirection: 'asc' | 'desc';
pageIndex: number;
size: number;
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
transactionType: string;
latencyAggregationType: LatencyAggregationType;
}) {
const { apmEventClient, start, end, esFilter } = setup;
const field = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
);
@ -87,11 +69,15 @@ export async function getTransactionGroupsForPage({
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: transactionType } },
{ range: rangeFilter(start, end) },
...getDocumentTypeFilterForAggregatedTransactions(
searchAggregatedTransactions
),
...esFilter,
],
},
},
aggs: {
total_duration: { sum: { field } },
transaction_groups: {
terms: {
field: TRANSACTION_NAME,
@ -99,9 +85,15 @@ export async function getTransactionGroupsForPage({
order: { _count: 'desc' },
},
aggs: {
transaction_group_total_duration: {
sum: { field },
},
...getLatencyAggregation(latencyAggregationType, field),
[EVENT_OUTCOME]: {
filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
terms: {
field: EVENT_OUTCOME,
include: [EventOutcome.failure, EventOutcome.success],
},
},
},
},
@ -109,12 +101,16 @@ export async function getTransactionGroupsForPage({
},
});
const totalDuration = response.aggregations?.total_duration.value;
const transactionGroups =
response.aggregations?.transaction_groups.buckets.map((bucket) => {
const errorRate =
bucket.doc_count > 0
? bucket[EVENT_OUTCOME].doc_count / bucket.doc_count
: null;
const errorRate = calculateTransactionErrorPercentage(
bucket[EVENT_OUTCOME]
);
const transactionGroupTotalDuration =
bucket.transaction_group_total_duration.value || 0;
return {
name: bucket.key as string,
@ -128,36 +124,17 @@ export async function getTransactionGroupsForPage({
value: bucket.doc_count,
}),
errorRate,
impact: totalDuration
? (transactionGroupTotalDuration * 100) / totalDuration
: 0,
};
}) ?? [];
const totalDurationValues = transactionGroups.map(
(group) => (group.latency ?? 0) * group.throughput
);
const minTotalDuration = Math.min(...totalDurationValues);
const maxTotalDuration = Math.max(...totalDurationValues);
const transactionGroupsWithImpact = transactionGroups.map((group) => ({
...group,
impact:
(((group.latency ?? 0) * group.throughput - minTotalDuration) /
(maxTotalDuration - minTotalDuration)) *
100,
}));
// Sort transaction groups first, and only get timeseries for data in view.
// This is to limit the possibility of creating too many buckets.
const sortedAndSlicedTransactionGroups = orderBy(
transactionGroupsWithImpact,
sortField,
[sortDirection]
).slice(pageIndex * size, pageIndex * size + size);
return {
transactionGroups: sortedAndSlicedTransactionGroups,
totalTransactionGroups: transactionGroups.length,
transactionGroups: transactionGroups.map((transactionGroup) => ({
...transactionGroup,
transactionType,
})),
isAggregationAccurate:
(response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) ===
0,

View file

@ -1,119 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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';
import {
EVENT_OUTCOME,
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
import { ESFilter } from '../../../../../../typings/elasticsearch';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} 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
>;
export async function getTimeseriesDataForTransactionGroups({
apmEventClient,
start,
end,
serviceName,
transactionNames,
esFilter,
searchAggregatedTransactions,
size,
numBuckets,
transactionType,
latencyAggregationType,
}: {
apmEventClient: APMEventClient;
start: number;
end: number;
serviceName: string;
transactionNames: string[];
esFilter: ESFilter[];
searchAggregatedTransactions: boolean;
size: number;
numBuckets: number;
transactionType: string;
latencyAggregationType: LatencyAggregationType;
}) {
const { intervalString } = getBucketSize({ start, end, numBuckets });
const field = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
);
const timeseriesResponse = await apmEventClient.search({
apm: {
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ terms: { [TRANSACTION_NAME]: transactionNames } },
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: transactionType } },
{ range: rangeFilter(start, end) },
...getDocumentTypeFilterForAggregatedTransactions(
searchAggregatedTransactions
),
...esFilter,
],
},
},
aggs: {
transaction_groups: {
terms: {
field: TRANSACTION_NAME,
size,
},
aggs: {
timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval: intervalString,
min_doc_count: 0,
extended_bounds: {
min: start,
max: end,
},
},
aggs: {
...getLatencyAggregation(latencyAggregationType, field),
[EVENT_OUTCOME]: {
filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
},
},
},
},
},
},
},
});
return timeseriesResponse.aggregations?.transaction_groups.buckets ?? [];
}

View file

@ -1,89 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { getTimeseriesDataForTransactionGroups } from './get_timeseries_data_for_transaction_groups';
import {
getTransactionGroupsForPage,
ServiceOverviewTransactionGroupSortField,
} from './get_transaction_groups_for_page';
import { mergeTransactionGroupData } from './merge_transaction_group_data';
export async function getServiceTransactionGroups({
serviceName,
setup,
size,
numBuckets,
pageIndex,
sortDirection,
sortField,
searchAggregatedTransactions,
transactionType,
latencyAggregationType,
}: {
serviceName: string;
setup: Setup & SetupTimeRange;
size: number;
pageIndex: number;
numBuckets: number;
sortDirection: 'asc' | 'desc';
sortField: ServiceOverviewTransactionGroupSortField;
searchAggregatedTransactions: boolean;
transactionType: string;
latencyAggregationType: LatencyAggregationType;
}) {
const { apmEventClient, start, end, esFilter } = setup;
const {
transactionGroups,
totalTransactionGroups,
isAggregationAccurate,
} = await getTransactionGroupsForPage({
apmEventClient,
start,
end,
serviceName,
esFilter,
pageIndex,
sortField,
sortDirection,
size,
searchAggregatedTransactions,
transactionType,
latencyAggregationType,
});
const transactionNames = transactionGroups.map((group) => group.name);
const timeseriesData = await getTimeseriesDataForTransactionGroups({
apmEventClient,
start,
end,
esFilter,
numBuckets,
searchAggregatedTransactions,
serviceName,
size,
transactionNames,
transactionType,
latencyAggregationType,
});
return {
transactionGroups: mergeTransactionGroupData({
transactionGroups,
timeseriesData,
start,
end,
latencyAggregationType,
transactionType,
}),
totalTransactionGroups,
isAggregationAccurate,
};
}

View file

@ -1,90 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { calculateThroughput } from '../../helpers/calculate_throughput';
import { getLatencyValue } from '../../helpers/latency_aggregation_type';
import { TransactionGroupTimeseriesData } from './get_timeseries_data_for_transaction_groups';
import { TransactionGroupWithoutTimeseriesData } from './get_transaction_groups_for_page';
export function mergeTransactionGroupData({
start,
end,
transactionGroups,
timeseriesData,
latencyAggregationType,
transactionType,
}: {
start: number;
end: number;
transactionGroups: TransactionGroupWithoutTimeseriesData[];
timeseriesData: TransactionGroupTimeseriesData;
latencyAggregationType: LatencyAggregationType;
transactionType: string;
}) {
return transactionGroups.map((transactionGroup) => {
const groupBucket = timeseriesData.find(
({ key }) => key === transactionGroup.name
);
const timeseriesBuckets = groupBucket?.timeseries.buckets ?? [];
return timeseriesBuckets.reduce(
(acc, point) => {
return {
...acc,
latency: {
...acc.latency,
timeseries: acc.latency.timeseries.concat({
x: point.key,
y: getLatencyValue({
latencyAggregationType,
aggregation: point.latency,
}),
}),
},
throughput: {
...acc.throughput,
timeseries: acc.throughput.timeseries.concat({
x: point.key,
y: calculateThroughput({
start,
end,
value: point.doc_count,
}),
}),
},
errorRate: {
...acc.errorRate,
timeseries: acc.errorRate.timeseries.concat({
x: point.key,
y: point[EVENT_OUTCOME].doc_count / point.doc_count,
}),
},
};
},
{
name: transactionGroup.name,
transactionType,
latency: {
value: transactionGroup.latency,
timeseries: [] as Array<{ x: number; y: number | null }>,
},
throughput: {
value: transactionGroup.throughput,
timeseries: [] as Array<{ x: number; y: number }>,
},
errorRate: {
value: transactionGroup.errorRate,
timeseries: [] as Array<{ x: number; y: number | null }>,
},
impact: transactionGroup.impact,
}
);
});
}

View file

@ -61,9 +61,10 @@ import {
transactionChartsDistributionRoute,
transactionChartsErrorRateRoute,
transactionGroupsRoute,
transactionGroupsOverviewRoute,
transactionGroupsPrimaryStatisticsRoute,
transactionLatencyChatsRoute,
transactionThroughputChatsRoute,
transactionGroupsComparisonStatisticsRoute,
} from './transactions';
import {
rumOverviewLocalFiltersRoute,
@ -164,9 +165,10 @@ const createApmApi = () => {
.add(transactionChartsDistributionRoute)
.add(transactionChartsErrorRateRoute)
.add(transactionGroupsRoute)
.add(transactionGroupsOverviewRoute)
.add(transactionGroupsPrimaryStatisticsRoute)
.add(transactionLatencyChatsRoute)
.add(transactionThroughputChatsRoute)
.add(transactionGroupsComparisonStatisticsRoute)
// UI filters
.add(uiFiltersEnvironmentsRoute)

View file

@ -7,27 +7,29 @@
import Boom from '@hapi/boom';
import * as t from 'io-ts';
import { createRoute } from './create_route';
import { rangeRt, uiFiltersRt } from './default_api_types';
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups';
import { getTransactionBreakdown } from '../lib/transactions/breakdown';
import { getAnomalySeries } from '../lib/transactions/get_anomaly_data';
import { getTransactionDistribution } from '../lib/transactions/distribution';
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';
import { jsonRt } from '../../common/runtime_types/json_rt';
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups';
import { getServiceTransactionGroupComparisonStatistics } from '../lib/services/get_service_transaction_group_comparison_statistics';
import { getTransactionBreakdown } from '../lib/transactions/breakdown';
import { getTransactionDistribution } from '../lib/transactions/distribution';
import { getAnomalySeries } from '../lib/transactions/get_anomaly_data';
import { getLatencyTimeseries } from '../lib/transactions/get_latency_charts';
import { getThroughputCharts } from '../lib/transactions/get_throughput_charts';
import { getTransactionGroupList } from '../lib/transaction_groups';
import { getErrorRate } from '../lib/transaction_groups/get_error_rate';
import { createRoute } from './create_route';
import { rangeRt, uiFiltersRt } from './default_api_types';
/**
* Returns a list of transactions grouped by name
* //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/overview/
* //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/primary_statistics/
*/
export const transactionGroupsRoute = createRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups',
@ -63,25 +65,56 @@ export const transactionGroupsRoute = createRoute({
},
});
export const transactionGroupsOverviewRoute = createRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups/overview',
export const transactionGroupsPrimaryStatisticsRoute = createRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics',
params: t.type({
path: t.type({ serviceName: t.string }),
query: t.intersection([
rangeRt,
uiFiltersRt,
t.type({
size: toNumberRt,
transactionType: t.string,
latencyAggregationType: latencyAggregationTypeRt,
}),
]),
}),
options: {
tags: ['access:apm'],
},
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
const {
path: { serviceName },
query: { latencyAggregationType, transactionType },
} = context.params;
return getServiceTransactionGroups({
setup,
serviceName,
searchAggregatedTransactions,
transactionType,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
});
},
});
export const transactionGroupsComparisonStatisticsRoute = createRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics',
params: t.type({
path: t.type({ serviceName: t.string }),
query: t.intersection([
rangeRt,
uiFiltersRt,
t.type({
transactionNames: jsonRt,
numBuckets: toNumberRt,
pageIndex: toNumberRt,
sortDirection: t.union([t.literal('asc'), t.literal('desc')]),
sortField: t.union([
t.literal('name'),
t.literal('latency'),
t.literal('throughput'),
t.literal('errorRate'),
t.literal('impact'),
]),
transactionType: t.string,
latencyAggregationType: latencyAggregationTypeRt,
}),
@ -100,24 +133,18 @@ export const transactionGroupsOverviewRoute = createRoute({
const {
path: { serviceName },
query: {
transactionNames,
latencyAggregationType,
numBuckets,
pageIndex,
size,
sortDirection,
sortField,
transactionType,
},
} = context.params;
return getServiceTransactionGroups({
return getServiceTransactionGroupComparisonStatistics({
setup,
serviceName,
pageIndex,
transactionNames,
searchAggregatedTransactions,
size,
sortDirection,
sortField,
transactionType,
numBuckets,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,

View file

@ -62,7 +62,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
loadTestFile(require.resolve('./transactions/latency'));
loadTestFile(require.resolve('./transactions/throughput'));
loadTestFile(require.resolve('./transactions/top_transaction_groups'));
loadTestFile(require.resolve('./transactions/transactions_groups_overview'));
loadTestFile(require.resolve('./transactions/transactions_groups_primary_statistics'));
loadTestFile(require.resolve('./transactions/transactions_groups_comparison_statistics'));
loadTestFile(require.resolve('./feature_controls'));

View file

@ -6,20 +6,17 @@
*/
import expect from '@kbn/expect';
import { last, omit, pick, sortBy } from 'lodash';
import url from 'url';
import { sortBy, pick, last, omit } from 'lodash';
import { ValuesType } from 'utility-types';
import { registry } from '../../../common/registry';
import { Maybe } from '../../../../../plugins/apm/typings/common';
import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number';
import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi';
import { roundNumber } from '../../../utils';
import { ENVIRONMENT_ALL } from '../../../../../plugins/apm/common/environment_filter_values';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi';
import archives from '../../../common/fixtures/es_archiver/archives_metadata';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { registry } from '../../../common/registry';
import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils';
const round = (num: Maybe<number>): string => (isFiniteNumber(num) ? num.toPrecision(4) : '');
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const es = getService('es');
@ -235,9 +232,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(opbeansNode !== undefined).to.be(true);
const values = {
latency: round(opbeansNode?.latency.value),
throughput: round(opbeansNode?.throughput.value),
errorRate: round(opbeansNode?.errorRate.value),
latency: roundNumber(opbeansNode?.latency.value),
throughput: roundNumber(opbeansNode?.throughput.value),
errorRate: roundNumber(opbeansNode?.errorRate.value),
...pick(opbeansNode, 'serviceName', 'type', 'agentName', 'environment', 'impact'),
};
@ -250,16 +247,16 @@ export default function ApiTest({ getService }: FtrProviderContext) {
environment: '',
serviceName: 'opbeans-node',
type: 'service',
errorRate: round(errors / count),
latency: round(sum / count),
throughput: round(count / ((endTime - startTime) / 1000 / 60)),
errorRate: roundNumber(errors / count),
latency: roundNumber(sum / count),
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
impact: 100,
});
const firstValue = round(opbeansNode?.latency.timeseries[0].y);
const lastValue = round(last(opbeansNode?.latency.timeseries)?.y);
const firstValue = roundNumber(opbeansNode?.latency.timeseries[0].y);
const lastValue = roundNumber(last(opbeansNode?.latency.timeseries)?.y);
expect(firstValue).to.be(round(20 / 3));
expect(firstValue).to.be(roundNumber(20 / 3));
expect(lastValue).to.be('1.000');
});
@ -271,9 +268,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(postgres !== undefined).to.be(true);
const values = {
latency: round(postgres?.latency.value),
throughput: round(postgres?.throughput.value),
errorRate: round(postgres?.errorRate.value),
latency: roundNumber(postgres?.latency.value),
throughput: roundNumber(postgres?.throughput.value),
errorRate: roundNumber(postgres?.errorRate.value),
...pick(postgres, 'spanType', 'spanSubtype', 'name', 'impact', 'type'),
};
@ -286,9 +283,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
spanSubtype: 'http',
name: 'postgres',
type: 'external',
errorRate: round(errors / count),
latency: round(sum / count),
throughput: round(count / ((endTime - startTime) / 1000 / 60)),
errorRate: roundNumber(errors / count),
latency: roundNumber(sum / count),
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
impact: 0,
});
});

View file

@ -0,0 +1,517 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data 1`] = `
Array [
Object {
"x": 1607435820000,
"y": null,
},
Object {
"x": 1607435880000,
"y": 69429,
},
Object {
"x": 1607435940000,
"y": 8071285,
},
Object {
"x": 1607436000000,
"y": 31949,
},
Object {
"x": 1607436060000,
"y": null,
},
Object {
"x": 1607436120000,
"y": 47755,
},
Object {
"x": 1607436180000,
"y": null,
},
Object {
"x": 1607436240000,
"y": 35403,
},
Object {
"x": 1607436300000,
"y": null,
},
Object {
"x": 1607436360000,
"y": null,
},
Object {
"x": 1607436420000,
"y": null,
},
Object {
"x": 1607436480000,
"y": 48137,
},
Object {
"x": 1607436540000,
"y": null,
},
Object {
"x": 1607436600000,
"y": 35457,
},
Object {
"x": 1607436660000,
"y": null,
},
Object {
"x": 1607436720000,
"y": null,
},
Object {
"x": 1607436780000,
"y": null,
},
Object {
"x": 1607436840000,
"y": null,
},
Object {
"x": 1607436900000,
"y": null,
},
Object {
"x": 1607436960000,
"y": 30501,
},
Object {
"x": 1607437020000,
"y": null,
},
Object {
"x": 1607437080000,
"y": null,
},
Object {
"x": 1607437140000,
"y": null,
},
Object {
"x": 1607437200000,
"y": 46937.5,
},
Object {
"x": 1607437260000,
"y": null,
},
Object {
"x": 1607437320000,
"y": null,
},
Object {
"x": 1607437380000,
"y": null,
},
Object {
"x": 1607437440000,
"y": null,
},
Object {
"x": 1607437500000,
"y": null,
},
Object {
"x": 1607437560000,
"y": null,
},
Object {
"x": 1607437620000,
"y": null,
},
]
`;
exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data 2`] = `
Array [
Object {
"x": 1607435820000,
"y": 0,
},
Object {
"x": 1607435880000,
"y": 1,
},
Object {
"x": 1607435940000,
"y": 2,
},
Object {
"x": 1607436000000,
"y": 1,
},
Object {
"x": 1607436060000,
"y": 0,
},
Object {
"x": 1607436120000,
"y": 1,
},
Object {
"x": 1607436180000,
"y": 0,
},
Object {
"x": 1607436240000,
"y": 4,
},
Object {
"x": 1607436300000,
"y": 0,
},
Object {
"x": 1607436360000,
"y": 0,
},
Object {
"x": 1607436420000,
"y": 0,
},
Object {
"x": 1607436480000,
"y": 2,
},
Object {
"x": 1607436540000,
"y": 0,
},
Object {
"x": 1607436600000,
"y": 1,
},
Object {
"x": 1607436660000,
"y": 0,
},
Object {
"x": 1607436720000,
"y": 0,
},
Object {
"x": 1607436780000,
"y": 0,
},
Object {
"x": 1607436840000,
"y": 0,
},
Object {
"x": 1607436900000,
"y": 0,
},
Object {
"x": 1607436960000,
"y": 2,
},
Object {
"x": 1607437020000,
"y": 0,
},
Object {
"x": 1607437080000,
"y": 0,
},
Object {
"x": 1607437140000,
"y": 0,
},
Object {
"x": 1607437200000,
"y": 2,
},
Object {
"x": 1607437260000,
"y": 0,
},
Object {
"x": 1607437320000,
"y": 0,
},
Object {
"x": 1607437380000,
"y": 0,
},
Object {
"x": 1607437440000,
"y": 0,
},
Object {
"x": 1607437500000,
"y": 0,
},
Object {
"x": 1607437560000,
"y": 0,
},
Object {
"x": 1607437620000,
"y": 0,
},
]
`;
exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct data 3`] = `
Array [
Object {
"x": 1607435820000,
"y": null,
},
Object {
"x": 1607435880000,
"y": 0,
},
Object {
"x": 1607435940000,
"y": 0,
},
Object {
"x": 1607436000000,
"y": 0,
},
Object {
"x": 1607436060000,
"y": null,
},
Object {
"x": 1607436120000,
"y": 0,
},
Object {
"x": 1607436180000,
"y": null,
},
Object {
"x": 1607436240000,
"y": 0,
},
Object {
"x": 1607436300000,
"y": null,
},
Object {
"x": 1607436360000,
"y": null,
},
Object {
"x": 1607436420000,
"y": null,
},
Object {
"x": 1607436480000,
"y": 0,
},
Object {
"x": 1607436540000,
"y": null,
},
Object {
"x": 1607436600000,
"y": 0,
},
Object {
"x": 1607436660000,
"y": null,
},
Object {
"x": 1607436720000,
"y": null,
},
Object {
"x": 1607436780000,
"y": null,
},
Object {
"x": 1607436840000,
"y": null,
},
Object {
"x": 1607436900000,
"y": null,
},
Object {
"x": 1607436960000,
"y": 0,
},
Object {
"x": 1607437020000,
"y": null,
},
Object {
"x": 1607437080000,
"y": null,
},
Object {
"x": 1607437140000,
"y": null,
},
Object {
"x": 1607437200000,
"y": 0.5,
},
Object {
"x": 1607437260000,
"y": null,
},
Object {
"x": 1607437320000,
"y": null,
},
Object {
"x": 1607437380000,
"y": null,
},
Object {
"x": 1607437440000,
"y": null,
},
Object {
"x": 1607437500000,
"y": null,
},
Object {
"x": 1607437560000,
"y": null,
},
Object {
"x": 1607437620000,
"y": null,
},
]
`;
exports[`APM API tests basic apm_8.0.0 Transaction groups comparison statistics when data is loaded returns the correct for latency aggregation 99th percentile 1`] = `
Array [
Object {
"x": 1607435820000,
"y": null,
},
Object {
"x": 1607435880000,
"y": 69429,
},
Object {
"x": 1607435940000,
"y": 8198285,
},
Object {
"x": 1607436000000,
"y": 31949,
},
Object {
"x": 1607436060000,
"y": null,
},
Object {
"x": 1607436120000,
"y": 47755,
},
Object {
"x": 1607436180000,
"y": null,
},
Object {
"x": 1607436240000,
"y": 73411,
},
Object {
"x": 1607436300000,
"y": null,
},
Object {
"x": 1607436360000,
"y": null,
},
Object {
"x": 1607436420000,
"y": null,
},
Object {
"x": 1607436480000,
"y": 55116,
},
Object {
"x": 1607436540000,
"y": null,
},
Object {
"x": 1607436600000,
"y": 35457,
},
Object {
"x": 1607436660000,
"y": null,
},
Object {
"x": 1607436720000,
"y": null,
},
Object {
"x": 1607436780000,
"y": null,
},
Object {
"x": 1607436840000,
"y": null,
},
Object {
"x": 1607436900000,
"y": null,
},
Object {
"x": 1607436960000,
"y": 46040,
},
Object {
"x": 1607437020000,
"y": null,
},
Object {
"x": 1607437080000,
"y": null,
},
Object {
"x": 1607437140000,
"y": null,
},
Object {
"x": 1607437200000,
"y": 82486,
},
Object {
"x": 1607437260000,
"y": null,
},
Object {
"x": 1607437320000,
"y": null,
},
Object {
"x": 1607437380000,
"y": null,
},
Object {
"x": 1607437440000,
"y": null,
},
Object {
"x": 1607437500000,
"y": null,
},
Object {
"x": 1607437560000,
"y": null,
},
Object {
"x": 1607437620000,
"y": null,
},
]
`;

View file

@ -0,0 +1,158 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import url from 'url';
import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi';
import archives from '../../common/fixtures/es_archiver/archives_metadata';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { registry } from '../../common/registry';
import { removeEmptyCoordinates, roundNumber } from '../../utils';
type TransactionsGroupsComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const archiveName = 'apm_8.0.0';
const { start, end } = archives[archiveName];
const transactionNames = ['DispatcherServlet#doGet', 'APIRestController#customers'];
registry.when(
'Transaction groups comparison statistics when data is not loaded',
{ config: 'basic', archives: [] },
() => {
it('handles the empty state', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`,
query: {
start,
end,
uiFilters: '{}',
numBuckets: 20,
latencyAggregationType: 'avg',
transactionType: 'request',
transactionNames: JSON.stringify(transactionNames),
},
})
);
expect(response.status).to.be(200);
expect(response.body).to.empty();
});
}
);
registry.when(
'Transaction groups comparison statistics when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
it('returns the correct data', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`,
query: {
start,
end,
uiFilters: '{}',
numBuckets: 20,
transactionType: 'request',
latencyAggregationType: 'avg',
transactionNames: JSON.stringify(transactionNames),
},
})
);
expect(response.status).to.be(200);
const transactionsGroupsComparisonStatistics = response.body as TransactionsGroupsComparisonStatistics;
expect(Object.keys(transactionsGroupsComparisonStatistics).length).to.be.eql(
transactionNames.length
);
transactionNames.map((transactionName) => {
expect(transactionsGroupsComparisonStatistics[transactionName]).not.to.be.empty();
});
const { latency, throughput, errorRate, impact } = transactionsGroupsComparisonStatistics[
transactionNames[0]
];
expect(removeEmptyCoordinates(latency).length).to.be.greaterThan(0);
expectSnapshot(latency).toMatch();
expect(removeEmptyCoordinates(throughput).length).to.be.greaterThan(0);
expectSnapshot(throughput).toMatch();
expect(removeEmptyCoordinates(errorRate).length).to.be.greaterThan(0);
expectSnapshot(errorRate).toMatch();
expectSnapshot(roundNumber(impact)).toMatchInline(`"93.93"`);
});
it('returns the correct for latency aggregation 99th percentile', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`,
query: {
start,
end,
uiFilters: '{}',
numBuckets: 20,
transactionType: 'request',
latencyAggregationType: 'p99',
transactionNames: JSON.stringify(transactionNames),
},
})
);
expect(response.status).to.be(200);
const transactionsGroupsComparisonStatistics = response.body as TransactionsGroupsComparisonStatistics;
expect(Object.keys(transactionsGroupsComparisonStatistics).length).to.be.eql(
transactionNames.length
);
transactionNames.map((transactionName) => {
expect(transactionsGroupsComparisonStatistics[transactionName]).not.to.be.empty();
});
const { latency, throughput, errorRate } = transactionsGroupsComparisonStatistics[
transactionNames[0]
];
expect(removeEmptyCoordinates(latency).length).to.be.greaterThan(0);
expectSnapshot(latency).toMatch();
expect(removeEmptyCoordinates(throughput).length).to.be.greaterThan(0);
expect(removeEmptyCoordinates(errorRate).length).to.be.greaterThan(0);
});
it('returns empty when transaction name is not found', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/comparison_statistics`,
query: {
start,
end,
uiFilters: '{}',
numBuckets: 20,
transactionType: 'request',
latencyAggregationType: 'avg',
transactionNames: JSON.stringify(['foo']),
},
})
);
expect(response.status).to.be(200);
expect(response.body).to.empty();
});
}
);
}

View file

@ -1,273 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { pick, uniqBy, sortBy } from 'lodash';
import url from 'url';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import archives from '../../common/fixtures/es_archiver/archives_metadata';
import { registry } from '../../common/registry';
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const archiveName = 'apm_8.0.0';
const { start, end } = archives[archiveName];
registry.when(
'Transaction groups overview when data is not loaded',
{ config: 'basic', archives: [] },
() => {
it('handles the empty state', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`,
query: {
start,
end,
uiFilters: '{}',
size: 5,
numBuckets: 20,
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
latencyAggregationType: 'avg',
transactionType: 'request',
},
})
);
expect(response.status).to.be(200);
expect(response.body).to.eql({
totalTransactionGroups: 0,
transactionGroups: [],
isAggregationAccurate: true,
});
});
}
);
registry.when(
'Top transaction groups when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
it('returns the correct data', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`,
query: {
start,
end,
uiFilters: '{}',
size: 5,
numBuckets: 20,
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
transactionType: 'request',
latencyAggregationType: 'avg',
},
})
);
expect(response.status).to.be(200);
expectSnapshot(response.body.totalTransactionGroups).toMatchInline(`12`);
expectSnapshot(response.body.transactionGroups.map((group: any) => group.name))
.toMatchInline(`
Array [
"DispatcherServlet#doGet",
"APIRestController#customers",
"APIRestController#order",
"APIRestController#stats",
"APIRestController#customerWhoBought",
]
`);
expectSnapshot(response.body.transactionGroups.map((group: any) => group.impact))
.toMatchInline(`
Array [
100,
1.43059146953109,
0.953769516915408,
0.905498741191481,
0.894989230293471,
]
`);
const firstItem = response.body.transactionGroups[0];
expectSnapshot(
pick(firstItem, 'name', 'latency.value', 'throughput.value', 'errorRate.value', 'impact')
).toMatchInline(`
Object {
"errorRate": Object {
"value": 0.0625,
},
"impact": 100,
"latency": Object {
"value": 1044995.1875,
},
"name": "DispatcherServlet#doGet",
"throughput": Object {
"value": 0.533333333333333,
},
}
`);
expectSnapshot(
firstItem.latency.timeseries.filter(({ y }: any) => y > 0).length
).toMatchInline(`9`);
expectSnapshot(
firstItem.throughput.timeseries.filter(({ y }: any) => y > 0).length
).toMatchInline(`9`);
expectSnapshot(
firstItem.errorRate.timeseries.filter(({ y }: any) => y > 0).length
).toMatchInline(`1`);
});
it('sorts items in the correct order', async () => {
const descendingResponse = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`,
query: {
start,
end,
uiFilters: '{}',
size: 5,
numBuckets: 20,
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
transactionType: 'request',
latencyAggregationType: 'avg',
},
})
);
expect(descendingResponse.status).to.be(200);
const descendingOccurrences = descendingResponse.body.transactionGroups.map(
(item: any) => item.impact
);
expect(descendingOccurrences).to.eql(sortBy(descendingOccurrences.concat()).reverse());
const ascendingResponse = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`,
query: {
start,
end,
uiFilters: '{}',
size: 5,
numBuckets: 20,
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
transactionType: 'request',
latencyAggregationType: 'avg',
},
})
);
const ascendingOccurrences = ascendingResponse.body.transactionGroups.map(
(item: any) => item.impact
);
expect(ascendingOccurrences).to.eql(sortBy(ascendingOccurrences.concat()).reverse());
});
it('sorts items by the correct field', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`,
query: {
start,
end,
uiFilters: '{}',
size: 5,
numBuckets: 20,
pageIndex: 0,
sortDirection: 'desc',
sortField: 'latency',
transactionType: 'request',
latencyAggregationType: 'avg',
},
})
);
expect(response.status).to.be(200);
const latencies = response.body.transactionGroups.map((group: any) => group.latency.value);
expect(latencies).to.eql(sortBy(latencies.concat()).reverse());
});
it('paginates through the items', async () => {
const size = 1;
const firstPage = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/overview`,
query: {
start,
end,
uiFilters: '{}',
size,
numBuckets: 20,
pageIndex: 0,
sortDirection: 'desc',
sortField: 'impact',
transactionType: 'request',
latencyAggregationType: 'avg',
},
})
);
expect(firstPage.status).to.eql(200);
const totalItems = firstPage.body.totalTransactionGroups;
const pages = Math.floor(totalItems / size);
const items = await new Array(pages)
.fill(undefined)
.reduce(async (prevItemsPromise, _, pageIndex) => {
const prevItems = await prevItemsPromise;
const thisPage = await supertest.get(
url.format({
pathname: '/api/apm/services/opbeans-java/transactions/groups/overview',
query: {
start,
end,
uiFilters: '{}',
size,
numBuckets: 20,
pageIndex,
sortDirection: 'desc',
sortField: 'impact',
transactionType: 'request',
latencyAggregationType: 'avg',
},
})
);
return prevItems.concat(thisPage.body.transactionGroups);
}, Promise.resolve([]));
expect(items.length).to.eql(totalItems);
expect(uniqBy(items, 'name').length).to.eql(totalItems);
});
}
);
}

View file

@ -0,0 +1,150 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { pick, sum } from 'lodash';
import url from 'url';
import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import archives from '../../common/fixtures/es_archiver/archives_metadata';
import { registry } from '../../common/registry';
type TransactionsGroupsPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const archiveName = 'apm_8.0.0';
const { start, end } = archives[archiveName];
registry.when(
'Transaction groups primary statistics when data is not loaded',
{ config: 'basic', archives: [] },
() => {
it('handles the empty state', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/primary_statistics`,
query: {
start,
end,
uiFilters: '{}',
latencyAggregationType: 'avg',
transactionType: 'request',
},
})
);
expect(response.status).to.be(200);
const transctionsGroupsPrimaryStatistics = response.body as TransactionsGroupsPrimaryStatistics;
expect(transctionsGroupsPrimaryStatistics.transactionGroups).to.empty();
expect(transctionsGroupsPrimaryStatistics.isAggregationAccurate).to.be(true);
});
}
);
registry.when(
'Transaction groups primary statistics when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
it('returns the correct data', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/primary_statistics`,
query: {
start,
end,
uiFilters: '{}',
transactionType: 'request',
latencyAggregationType: 'avg',
},
})
);
expect(response.status).to.be(200);
const transctionsGroupsPrimaryStatistics = response.body as TransactionsGroupsPrimaryStatistics;
expectSnapshot(
transctionsGroupsPrimaryStatistics.transactionGroups.map((group: any) => group.name)
).toMatchInline(`
Array [
"DispatcherServlet#doGet",
"APIRestController#customerWhoBought",
"APIRestController#order",
"APIRestController#customer",
"ResourceHttpRequestHandler",
"APIRestController#customers",
"APIRestController#stats",
"APIRestController#topProducts",
"APIRestController#orders",
"APIRestController#product",
"APIRestController#products",
"DispatcherServlet#doPost",
]
`);
const impacts = transctionsGroupsPrimaryStatistics.transactionGroups.map(
(group: any) => group.impact
);
expectSnapshot(impacts).toMatchInline(`
Array [
93.9295870910491,
0.850308244392878,
0.905514602241759,
0.699947181217412,
0.143906183235671,
1.35334507158962,
0.860178761411346,
0.476138685202191,
0.446650726277923,
0.262571482598846,
0.062116281544223,
0.00973568923904662,
]
`);
expect(Math.round(sum(impacts))).to.eql(100);
const firstItem = transctionsGroupsPrimaryStatistics.transactionGroups[0];
expectSnapshot(pick(firstItem, 'name', 'latency', 'throughput', 'errorRate', 'impact'))
.toMatchInline(`
Object {
"errorRate": 0.0625,
"impact": 93.9295870910491,
"latency": 1044995.1875,
"name": "DispatcherServlet#doGet",
"throughput": 0.533333333333333,
}
`);
});
it('returns the correct data for latency aggregation 99th percentile', async () => {
const response = await supertest.get(
url.format({
pathname: `/api/apm/services/opbeans-java/transactions/groups/primary_statistics`,
query: {
start,
end,
uiFilters: '{}',
transactionType: 'request',
latencyAggregationType: 'p99',
},
})
);
expect(response.status).to.be(200);
const transctionsGroupsPrimaryStatistics = response.body as TransactionsGroupsPrimaryStatistics;
const firstItem = transctionsGroupsPrimaryStatistics.transactionGroups[0];
expectSnapshot(firstItem.latency).toMatchInline(`8198285`);
});
}
);
}

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Coordinate } from '../../plugins/apm/typings/timeseries';
import { isFiniteNumber } from '../../plugins/apm/common/utils/is_finite_number';
import { Maybe } from '../../plugins/apm/typings/common';
export function roundNumber(num: Maybe<number>) {
return isFiniteNumber(num) ? num.toPrecision(4) : '';
}
export function removeEmptyCoordinates(coordinates: Coordinate[]) {
return coordinates.filter(({ y }) => isFiniteNumber(y));
}

View file

@ -190,6 +190,15 @@ export interface AggregationOptionsByType {
gap_policy?: 'skip' | 'insert_zeros';
format?: string;
};
rate: {
unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
} & (
| {
field: string;
mode: 'sum' | 'value_count';
}
| {}
);
}
type AggregationType = keyof AggregationOptionsByType;
@ -409,6 +418,9 @@ interface AggregationResponsePart<TAggregationOptionsMap extends AggregationOpti
avg_bucket: {
value: number | null;
};
rate: {
value: number | null;
};
}
type TopMetricsMap<TFieldName> = TFieldName extends string