[APM] Backend operations detail view + metric charts (#133866)

* Metric charts for backend operation detail view

* API tests

* Review feedback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2022-06-10 19:14:47 +02:00 committed by GitHub
parent 3afe960302
commit 7604d3beef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1279 additions and 283 deletions

View file

@ -31,7 +31,7 @@ export function getSpanDestinationMetrics(events: ApmFields[]) {
return {
...metricset.key,
['metricset.name']: 'span_destination',
['metricset.name']: 'service_destination',
'span.destination.service.response_time.sum.us': sum,
'span.destination.service.response_time.count': count,
};

View file

@ -68,7 +68,7 @@ describe('span destination metrics', () => {
)
)
)
.filter((fields) => fields['metricset.name'] === 'span_destination');
.filter((fields) => fields['metricset.name'] === 'service_destination');
});
it('generates the right amount of span metrics', () => {

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export interface TimeRangeMetadata {
isUsingServiceDestinationMetrics: boolean;
}

View file

@ -4,49 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useBackendDetailOperationsBreadcrumb } from '../../../hooks/use_backend_detail_operations_breadcrumb';
import { BackendDetailOperationsList } from './backend_detail_operations_list';
export function BackendDetailOperations() {
const {
query: {
backendName,
rangeFrom,
rangeTo,
refreshInterval,
refreshPaused,
environment,
kuery,
comparisonEnabled,
},
} = useApmParams('/backends/operations');
const apmRouter = useApmRouter();
useBreadcrumb([
{
title: i18n.translate(
'xpack.apm.backendDetailOperations.breadcrumbTitle',
{ defaultMessage: 'Operations' }
),
href: apmRouter.link('/backends/operations', {
query: {
backendName,
rangeFrom,
rangeTo,
refreshInterval,
refreshPaused,
environment,
kuery,
comparisonEnabled,
},
}),
},
]);
useBackendDetailOperationsBreadcrumb();
return <BackendDetailOperationsList />;
}

View file

@ -4,22 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexItem } from '@elastic/eui';
import { EuiPanel } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { BackendLatencyChart } from './backend_latency_chart';
import { BackendDetailDependenciesTable } from './backend_detail_dependencies_table';
import { BackendThroughputChart } from './backend_throughput_chart';
import { BackendFailedTransactionRateChart } from './backend_error_rate_chart';
import { useBreakpoints } from '../../../hooks/use_breakpoints';
import { BackendMetricCharts } from '../../shared/backend_metric_charts';
export function BackendDetailOverview() {
const {
@ -56,54 +49,11 @@ export function BackendDetailOverview() {
}),
},
]);
const largeScreenOrSmaller = useBreakpoints().isLarge;
return (
<>
<ChartPointerEventContextProvider>
<EuiFlexGroup
direction={largeScreenOrSmaller ? 'column' : 'row'}
gutterSize="s"
>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.backendDetailLatencyChartTitle', {
defaultMessage: 'Latency',
})}
</h2>
</EuiTitle>
<BackendLatencyChart height={200} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.backendDetailThroughputChartTitle',
{ defaultMessage: 'Throughput' }
)}
</h2>
</EuiTitle>
<BackendThroughputChart height={200} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.backendDetailFailedTransactionRateChartTitle',
{ defaultMessage: 'Failed transaction rate' }
)}
</h2>
</EuiTitle>
<BackendFailedTransactionRateChart height={200} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<BackendMetricCharts />
</ChartPointerEventContextProvider>
<EuiSpacer size="l" />
<BackendDetailDependenciesTable />

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useBackendDetailOperationsBreadcrumb } from '../../../hooks/use_backend_detail_operations_breadcrumb';
import { BackendMetricCharts } from '../../shared/backend_metric_charts';
import { DetailViewHeader } from '../../shared/detail_view_header';
export function BackendOperationDetailView() {
const router = useApmRouter();
const {
query: { spanName, ...query },
} = useApmParams('/backends/operation');
useBackendDetailOperationsBreadcrumb();
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<DetailViewHeader
backLabel={i18n.translate(
'xpack.apm.backendOperationDetailView.header.backLinkLabel',
{ defaultMessage: 'All operations' }
)}
backHref={router.link('/backends/operations', { query })}
title={spanName}
/>
</EuiFlexItem>
<EuiSpacer size="s" />
<EuiFlexItem>
<ChartPointerEventContextProvider>
<BackendMetricCharts />
</ChartPointerEventContextProvider>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -30,6 +30,8 @@ import { BackendDetailOperations } from '../../app/backend_detail_operations';
import { BackendDetailView } from '../../app/backend_detail_view';
import { RedirectPathBackendDetailView } from './redirect_path_backend_detail_view';
import { RedirectBackendsToBackendDetailOverview } from './redirect_backends_to_backend_detail_view';
import { BackendOperationDetailView } from '../../app/backend_operation_detail_view';
import { TimeRangeMetadataContextProvider } from '../../../context/time_range_metadata/time_range_metadata_context';
function page<
TPath extends string,
@ -69,6 +71,7 @@ function page<
</Breadcrumb>
),
children,
params,
},
} as any;
}
@ -149,7 +152,11 @@ export const DependenciesOperationsTitle = i18n.translate(
export const home = {
'/': {
element: <Outlet />,
element: (
<TimeRangeMetadataContextProvider>
<Outlet />
</TimeRangeMetadataContextProvider>
),
params: t.type({
query: t.intersection([
environmentRt,
@ -273,16 +280,15 @@ export const home = {
children: {
'/backends/operations': {
element: <BackendDetailOperations />,
children: {
'/backends/operation': {
params: t.type({
query: t.type({
spanName: t.string,
}),
}),
element: <Outlet />,
},
},
},
'/backends/operation': {
params: t.type({
query: t.type({
spanName: t.string,
}),
}),
element: <BackendOperationDetailView />,
},
'/backends/overview': {
element: <BackendDetailOverview />,

View file

@ -29,6 +29,7 @@ import { ServiceLogs } from '../../app/service_logs';
import { InfraOverview } from '../../app/infra_overview';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { offsetRt } from '../../../../common/comparison_rt';
import { TimeRangeMetadataContextProvider } from '../../../context/time_range_metadata/time_range_metadata_context';
function page({
title,
@ -63,7 +64,11 @@ function page({
export const serviceDetail = {
'/services/{serviceName}': {
element: <ApmServiceWrapper />,
element: (
<TimeRangeMetadataContextProvider>
<ApmServiceWrapper />
</TimeRangeMetadataContextProvider>
),
params: t.intersection([
t.type({
path: t.type({

View file

@ -87,7 +87,8 @@ export function BackendDetailTemplate({ children }: Props) {
label: i18n.translate('xpack.apm.backendDetailOperations.title', {
defaultMessage: 'Operations',
}),
isSelected: path === '/backends/operations',
isSelected:
path === '/backends/operations' || path === '/backends/operation',
},
]
: [];

View file

@ -7,18 +7,19 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { usePreviousPeriodLabel } from '../../../hooks/use_previous_period_text';
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
import { isTimeComparison } from '../time_comparison/get_comparison_options';
import { asPercent } from '../../../../common/utils/formatters';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { Coordinate, TimeSeries } from '../../../../typings/timeseries';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
import { useApmParams } from '../../../hooks/use_apm_params';
import { TimeseriesChart } from '../charts/timeseries_chart';
import {
ChartType,
getTimeSeriesColor,
} from '../../shared/charts/helper/get_timeseries_color';
import { getComparisonChartTheme } from '../../shared/time_comparison/get_comparison_chart_theme';
} from '../charts/helper/get_timeseries_color';
import { getComparisonChartTheme } from '../time_comparison/get_comparison_chart_theme';
import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params';
import { useSearchServiceDestinationMetrics } from '../../../context/time_range_metadata/use_search_service_destination_metrics';
function yLabelFormat(y?: number | null) {
return asPercent(y || 0, 1);
@ -26,28 +27,27 @@ function yLabelFormat(y?: number | null) {
export function BackendFailedTransactionRateChart({
height,
backendName,
kuery,
environment,
rangeFrom,
rangeTo,
offset,
comparisonEnabled,
spanName,
}: {
height: number;
}) {
const {
query: {
backendName,
kuery,
environment,
rangeFrom,
rangeTo,
offset,
comparisonEnabled,
},
} = useApmParams('/backends/overview');
} & BackendMetricChartsRouteParams) {
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const comparisonChartTheme = getComparisonChartTheme();
const { isTimeRangeMetadataLoading, searchServiceDestinationMetrics } =
useSearchServiceDestinationMetrics({ rangeFrom, rangeTo, kuery });
const { data, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
if (isTimeRangeMetadataLoading) {
return;
}
@ -63,11 +63,24 @@ export function BackendFailedTransactionRateChart({
: undefined,
kuery,
environment,
spanName: spanName || '',
searchServiceDestinationMetrics,
},
},
});
},
[backendName, start, end, offset, kuery, environment, comparisonEnabled]
[
backendName,
start,
end,
offset,
kuery,
environment,
comparisonEnabled,
spanName,
isTimeRangeMetadataLoading,
searchServiceDestinationMetrics,
]
);
const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(

View file

@ -7,43 +7,45 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { usePreviousPeriodLabel } from '../../../hooks/use_previous_period_text';
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
import { isTimeComparison } from '../time_comparison/get_comparison_options';
import { getDurationFormatter } from '../../../../common/utils/formatters';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { Coordinate, TimeSeries } from '../../../../typings/timeseries';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
import { TimeseriesChart } from '../charts/timeseries_chart';
import {
getMaxY,
getResponseTimeTickFormatter,
} from '../../shared/charts/transaction_charts/helper';
import { useApmParams } from '../../../hooks/use_apm_params';
} from '../charts/transaction_charts/helper';
import {
ChartType,
getTimeSeriesColor,
} from '../../shared/charts/helper/get_timeseries_color';
import { getComparisonChartTheme } from '../../shared/time_comparison/get_comparison_chart_theme';
export function BackendLatencyChart({ height }: { height: number }) {
const {
query: {
backendName,
rangeFrom,
rangeTo,
kuery,
environment,
offset,
comparisonEnabled,
},
} = useApmParams('/backends/overview');
} from '../charts/helper/get_timeseries_color';
import { getComparisonChartTheme } from '../time_comparison/get_comparison_chart_theme';
import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params';
import { useSearchServiceDestinationMetrics } from '../../../context/time_range_metadata/use_search_service_destination_metrics';
export function BackendLatencyChart({
height,
backendName,
rangeFrom,
rangeTo,
kuery,
environment,
offset,
comparisonEnabled,
spanName,
}: { height: number } & BackendMetricChartsRouteParams) {
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const comparisonChartTheme = getComparisonChartTheme();
const { isTimeRangeMetadataLoading, searchServiceDestinationMetrics } =
useSearchServiceDestinationMetrics({ rangeFrom, rangeTo, kuery });
const { data, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
if (isTimeRangeMetadataLoading) {
return;
}
@ -59,11 +61,24 @@ export function BackendLatencyChart({ height }: { height: number }) {
: undefined,
kuery,
environment,
spanName: spanName || '',
searchServiceDestinationMetrics,
},
},
});
},
[backendName, start, end, offset, kuery, environment, comparisonEnabled]
[
backendName,
start,
end,
offset,
kuery,
environment,
comparisonEnabled,
spanName,
isTimeRangeMetadataLoading,
searchServiceDestinationMetrics,
]
);
const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TypeOf } from '@kbn/typed-react-router-config';
import { ApmRoutes } from '../../routing/apm_route_config';
export type BackendMetricChartsRouteParams = Pick<
{ spanName?: string } & TypeOf<
ApmRoutes,
'/backends/operation' | '/backends/overview'
>['query'],
| 'backendName'
| 'comparisonEnabled'
| 'spanName'
| 'rangeFrom'
| 'rangeTo'
| 'kuery'
| 'environment'
| 'comparisonEnabled'
| 'offset'
>;

View file

@ -7,39 +7,41 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { usePreviousPeriodLabel } from '../../../hooks/use_previous_period_text';
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
import { isTimeComparison } from '../time_comparison/get_comparison_options';
import { asTransactionRate } from '../../../../common/utils/formatters';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { Coordinate, TimeSeries } from '../../../../typings/timeseries';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
import { useApmParams } from '../../../hooks/use_apm_params';
import { TimeseriesChart } from '../charts/timeseries_chart';
import {
ChartType,
getTimeSeriesColor,
} from '../../shared/charts/helper/get_timeseries_color';
import { getComparisonChartTheme } from '../../shared/time_comparison/get_comparison_chart_theme';
export function BackendThroughputChart({ height }: { height: number }) {
const {
query: {
backendName,
rangeFrom,
rangeTo,
kuery,
environment,
offset,
comparisonEnabled,
},
} = useApmParams('/backends/overview');
} from '../charts/helper/get_timeseries_color';
import { getComparisonChartTheme } from '../time_comparison/get_comparison_chart_theme';
import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params';
import { useSearchServiceDestinationMetrics } from '../../../context/time_range_metadata/use_search_service_destination_metrics';
export function BackendThroughputChart({
height,
backendName,
rangeFrom,
rangeTo,
kuery,
environment,
offset,
comparisonEnabled,
spanName,
}: { height: number } & BackendMetricChartsRouteParams) {
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const comparisonChartTheme = getComparisonChartTheme();
const { isTimeRangeMetadataLoading, searchServiceDestinationMetrics } =
useSearchServiceDestinationMetrics({ rangeFrom, rangeTo, kuery });
const { data, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
if (isTimeRangeMetadataLoading) {
return;
}
@ -55,11 +57,24 @@ export function BackendThroughputChart({ height }: { height: number }) {
: undefined,
kuery,
environment,
spanName: spanName || '',
searchServiceDestinationMetrics,
},
},
});
},
[backendName, start, end, offset, kuery, environment, comparisonEnabled]
[
backendName,
start,
end,
offset,
kuery,
environment,
comparisonEnabled,
spanName,
isTimeRangeMetadataLoading,
searchServiceDestinationMetrics,
]
);
const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(

View file

@ -0,0 +1,91 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
import { useBreakpoints } from '../../../hooks/use_breakpoints';
import { BackendFailedTransactionRateChart } from './backend_error_rate_chart';
import { BackendLatencyChart } from './backend_latency_chart';
import { BackendMetricChartsRouteParams } from './backend_metric_charts_route_params';
import { BackendThroughputChart } from './backend_throughput_chart';
export function BackendMetricCharts() {
const largeScreenOrSmaller = useBreakpoints().isLarge;
const {
query,
query: {
backendName,
rangeFrom,
rangeTo,
kuery,
environment,
comparisonEnabled,
offset,
},
} = useAnyOfApmParams('/backends/overview', '/backends/operation');
const spanName = 'spanName' in query ? query.spanName : undefined;
const props: BackendMetricChartsRouteParams = {
backendName,
rangeFrom,
rangeTo,
kuery,
environment,
comparisonEnabled,
offset,
spanName,
};
return (
<EuiFlexGroup
direction={largeScreenOrSmaller ? 'column' : 'row'}
gutterSize="s"
>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.backendDetailLatencyChartTitle', {
defaultMessage: 'Latency',
})}
</h2>
</EuiTitle>
<BackendLatencyChart height={200} {...props} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.backendDetailThroughputChartTitle', {
defaultMessage: 'Throughput',
})}
</h2>
</EuiTitle>
<BackendThroughputChart height={200} {...props} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.backendDetailFailedTransactionRateChartTitle',
{ defaultMessage: 'Failed transaction rate' }
)}
</h2>
</EuiTitle>
<BackendFailedTransactionRateChart height={200} {...props} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,45 @@
/*
* 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 React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiText,
EuiTitle,
} from '@elastic/eui';
export function DetailViewHeader({
backLabel,
backHref,
title,
}: {
backLabel: string;
backHref: string;
title: string;
}) {
return (
<EuiFlexGroup direction="column" gutterSize="m" alignItems="flexStart">
<EuiFlexItem>
<EuiLink href={backHref}>
<EuiFlexGroup direction="row" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type="arrowLeft" />
</EuiFlexItem>
<EuiFlexItem>{backLabel}</EuiFlexItem>
</EuiFlexGroup>
</EuiLink>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle>
<EuiText>{title}</EuiText>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,64 @@
/*
* 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 React, { createContext } from 'react';
import { TimeRangeMetadata } from '../../../common/time_range_metadata';
import { useApmParams } from '../../hooks/use_apm_params';
import { useApmRoutePath } from '../../hooks/use_apm_route_path';
import { FetcherResult, useFetcher } from '../../hooks/use_fetcher';
import { useTimeRange } from '../../hooks/use_time_range';
export const TimeRangeMetadataContext = createContext<
FetcherResult<TimeRangeMetadata> | undefined
>(undefined);
export function TimeRangeMetadataContextProvider({
children,
}: {
children: React.ReactElement;
}) {
const { query } = useApmParams('/*');
const kuery = 'kuery' in query ? query.kuery : '';
const range =
'rangeFrom' in query && 'rangeTo' in query
? { rangeFrom: query.rangeFrom, rangeTo: query.rangeTo }
: undefined;
if (!range) {
throw new Error('rangeFrom/rangeTo missing in URL');
}
const { start, end } = useTimeRange(range);
const routePath = useApmRoutePath();
const isOperationView =
routePath === '/backends/operation' || routePath === '/backends/operations';
const fetcherResult = useFetcher(
(callApmApi) => {
return callApmApi('GET /internal/apm/time_range_metadata', {
params: {
query: {
start,
end,
kuery,
useSpanName: isOperationView,
},
},
});
},
[start, end, kuery, isOperationView]
);
return (
<TimeRangeMetadataContext.Provider value={fetcherResult}>
{children}
</TimeRangeMetadataContext.Provider>
);
}

View file

@ -0,0 +1,31 @@
/*
* 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 { FETCH_STATUS } from '../../hooks/use_fetcher';
import { useTimeRangeMetadata } from './use_time_range_metadata_context';
export function useSearchServiceDestinationMetrics({
rangeFrom,
rangeTo,
kuery,
}: {
rangeFrom: string;
rangeTo: string;
kuery: string;
}) {
const { status, data } = useTimeRangeMetadata({
rangeFrom,
rangeTo,
kuery,
});
return {
isTimeRangeMetadataLoading: status === FETCH_STATUS.LOADING,
searchServiceDestinationMetrics:
data?.isUsingServiceDestinationMetrics ?? true,
};
}

View file

@ -0,0 +1,30 @@
/*
* 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 { useContext } from 'react';
import { TimeRangeMetadataContext } from './time_range_metadata_context';
export function useTimeRangeMetadata({
rangeFrom,
rangeTo,
kuery,
}: {
// require parameters to enforce type-safety. Only components
// with access to rangeFrom and rangeTo should be able to request
// time range metadata.
rangeFrom: string;
rangeTo: string;
kuery: string;
}) {
const context = useContext(TimeRangeMetadataContext);
if (!context) {
throw new Error('TimeRangeMetadataContext is not found');
}
return context;
}

View file

@ -0,0 +1,49 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { useBreadcrumb } from '../context/breadcrumbs/use_breadcrumb';
import { useAnyOfApmParams } from './use_apm_params';
import { useApmRouter } from './use_apm_router';
export function useBackendDetailOperationsBreadcrumb() {
const {
query: {
backendName,
rangeFrom,
rangeTo,
refreshInterval,
refreshPaused,
environment,
kuery,
comparisonEnabled,
},
} = useAnyOfApmParams('/backends/operations', '/backends/operation');
const apmRouter = useApmRouter();
useBreadcrumb([
{
title: i18n.translate(
'xpack.apm.backendDetailOperations.breadcrumbTitle',
{ defaultMessage: 'Operations' }
),
href: apmRouter.link('/backends/operations', {
query: {
backendName,
rangeFrom,
rangeTo,
refreshInterval,
refreshPaused,
environment,
kuery,
comparisonEnabled,
},
}),
},
]);
}

View file

@ -0,0 +1,94 @@
/*
* 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 {
kqlQuery,
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import {
METRICSET_NAME,
SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM,
SPAN_DURATION,
SPAN_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../../common/processor_event';
import { Setup } from '../setup_request';
export function getProcessorEventForServiceDestinationStatistics(
searchServiceDestinationMetrics: boolean
) {
return searchServiceDestinationMetrics
? ProcessorEvent.metric
: ProcessorEvent.span;
}
export function getDocumentTypeFilterForServiceDestinationStatistics(
searchServiceDestinationMetrics: boolean
) {
return searchServiceDestinationMetrics
? termQuery(METRICSET_NAME, 'service_destination')
: [];
}
export function getLatencyFieldForServiceDestinationStatistics(
searchServiceDestinationMetrics: boolean
) {
return searchServiceDestinationMetrics
? SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM
: SPAN_DURATION;
}
export function getDocCountFieldForServiceDestinationStatistics(
searchServiceDestinationMetrics: boolean
) {
return searchServiceDestinationMetrics
? SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT
: undefined;
}
export async function getIsUsingServiceDestinationMetrics({
setup,
useSpanName,
kuery,
start,
end,
}: {
setup: Setup;
useSpanName: boolean;
kuery: string;
start: number;
end: number;
}) {
const { apmEventClient } = setup;
const response = await apmEventClient.search(
'get_has_service_destination_metrics',
{
apm: {
events: [getProcessorEventForServiceDestinationStatistics(true)],
},
body: {
terminate_after: 1,
size: 1,
query: {
bool: {
filter: [
...rangeQuery(start, end),
...kqlQuery(kuery),
...getDocumentTypeFilterForServiceDestinationStatistics(true),
...(useSpanName ? [{ exists: { field: SPAN_NAME } }] : []),
],
},
},
},
}
);
return response.hits.total.value > 0;
}

View file

@ -5,24 +5,28 @@
* 2.0.
*/
import type {
ServerRouteRepository,
ReturnOf,
EndpointOf,
ReturnOf,
ServerRouteRepository,
} from '@kbn/server-route-repository';
import { PickByValue } from 'utility-types';
import { correlationsRouteRepository } from '../correlations/route';
import { agentKeysRouteRepository } from '../agent_keys/route';
import { alertsChartPreviewRouteRepository } from '../alerts/route';
import { backendsRouteRepository } from '../backends/route';
import { correlationsRouteRepository } from '../correlations/route';
import { dataViewRouteRepository } from '../data_view/route';
import { debugTelemetryRoute } from '../debug_telemetry/route';
import { environmentsRouteRepository } from '../environments/route';
import { errorsRouteRepository } from '../errors/route';
import { infrastructureRouteRepository } from '../infrastructure/route';
import { eventMetadataRouteRepository } from '../event_metadata/route';
import { fallbackToTransactionsRouteRepository } from '../fallback_to_transactions/route';
import { apmFleetRouteRepository } from '../fleet/route';
import { dataViewRouteRepository } from '../data_view/route';
import { historicalDataRouteRepository } from '../historical_data/route';
import { infrastructureRouteRepository } from '../infrastructure/route';
import { latencyDistributionRouteRepository } from '../latency_distribution/route';
import { metricsRouteRepository } from '../metrics/route';
import { observabilityOverviewRouteRepository } from '../observability_overview/route';
import { rumRouteRepository } from '../rum_client/route';
import { fallbackToTransactionsRouteRepository } from '../fallback_to_transactions/route';
import { serviceRouteRepository } from '../services/route';
import { serviceGroupRouteRepository } from '../service_groups/route';
import { serviceMapRouteRepository } from '../service_map/route';
@ -32,14 +36,12 @@ import { anomalyDetectionRouteRepository } from '../settings/anomaly_detection/r
import { apmIndicesRouteRepository } from '../settings/apm_indices/route';
import { customLinkRouteRepository } from '../settings/custom_link/route';
import { sourceMapsRouteRepository } from '../source_maps/route';
import { spanLinksRouteRepository } from '../span_links/route';
import { suggestionsRouteRepository } from '../suggestions/route';
import { timeRangeMetadataRoute } from '../time_range_metadata/route';
import { traceRouteRepository } from '../traces/route';
import { transactionRouteRepository } from '../transactions/route';
import { historicalDataRouteRepository } from '../historical_data/route';
import { eventMetadataRouteRepository } from '../event_metadata/route';
import { suggestionsRouteRepository } from '../suggestions/route';
import { agentKeysRouteRepository } from '../agent_keys/route';
import { spanLinksRouteRepository } from '../span_links/route';
import { debugTelemetryRoute } from '../debug_telemetry/route';
function getTypedGlobalApmServerRouteRepository() {
const repository = {
...dataViewRouteRepository,
@ -72,6 +74,7 @@ function getTypedGlobalApmServerRouteRepository() {
...spanLinksRouteRepository,
...infrastructureRouteRepository,
...debugTelemetryRoute,
...timeRangeMetadataRoute,
};
return repository;

View file

@ -5,33 +5,46 @@
* 2.0.
*/
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import {
kqlQuery,
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import { EventOutcome } from '../../../common/event_outcome';
import {
EVENT_OUTCOME,
SPAN_DESTINATION_SERVICE_RESOURCE,
SPAN_NAME,
} from '../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../common/utils/environment_query';
import { ProcessorEvent } from '../../../common/processor_event';
import { Setup } from '../../lib/helpers/setup_request';
import { getMetricsDateHistogramParams } from '../../lib/helpers/metrics';
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
import {
getDocCountFieldForServiceDestinationStatistics,
getDocumentTypeFilterForServiceDestinationStatistics,
getProcessorEventForServiceDestinationStatistics,
} from '../../lib/helpers/spans/get_is_using_service_destination_metrics';
export async function getErrorRateChartsForBackend({
backendName,
spanName,
setup,
start,
end,
environment,
kuery,
searchServiceDestinationMetrics,
offset,
}: {
backendName: string;
spanName: string;
setup: Setup;
start: number;
end: number;
environment: string;
kuery: string;
searchServiceDestinationMetrics: boolean;
offset?: string;
}) {
const { apmEventClient } = setup;
@ -44,7 +57,11 @@ export async function getErrorRateChartsForBackend({
const response = await apmEventClient.search('get_error_rate_for_backend', {
apm: {
events: [ProcessorEvent.metric],
events: [
getProcessorEventForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
],
},
body: {
size: 0,
@ -54,6 +71,10 @@ export async function getErrorRateChartsForBackend({
...environmentQuery(environment),
...kqlQuery(kuery),
...rangeQuery(startWithOffset, endWithOffset),
...termQuery(SPAN_NAME, spanName || null),
...getDocumentTypeFilterForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
{ term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } },
{
terms: {
@ -71,12 +92,37 @@ export async function getErrorRateChartsForBackend({
metricsInterval: 60,
}),
aggs: {
...(searchServiceDestinationMetrics
? {
total_count: {
sum: {
field: getDocCountFieldForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
},
},
}
: {}),
failures: {
filter: {
term: {
[EVENT_OUTCOME]: EventOutcome.failure,
},
},
aggs: {
...(searchServiceDestinationMetrics
? {
total_count: {
sum: {
field:
getDocCountFieldForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
},
},
}
: {}),
},
},
},
},
@ -86,8 +132,9 @@ export async function getErrorRateChartsForBackend({
return (
response.aggregations?.timeseries.buckets.map((bucket) => {
const totalCount = bucket.doc_count;
const failureCount = bucket.failures.doc_count;
const totalCount = bucket.total_count?.value ?? bucket.doc_count;
const failureCount =
bucket.failures.total_count?.value ?? bucket.failures.doc_count;
return {
x: bucket.key + offsetInMs,

View file

@ -5,20 +5,30 @@
* 2.0.
*/
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import {
kqlQuery,
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import {
SPAN_DESTINATION_SERVICE_RESOURCE,
SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM,
SPAN_NAME,
} from '../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../common/utils/environment_query';
import { ProcessorEvent } from '../../../common/processor_event';
import { Setup } from '../../lib/helpers/setup_request';
import { getMetricsDateHistogramParams } from '../../lib/helpers/metrics';
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
import {
getDocCountFieldForServiceDestinationStatistics,
getDocumentTypeFilterForServiceDestinationStatistics,
getLatencyFieldForServiceDestinationStatistics,
getProcessorEventForServiceDestinationStatistics,
} from '../../lib/helpers/spans/get_is_using_service_destination_metrics';
export async function getLatencyChartsForBackend({
backendName,
spanName,
searchServiceDestinationMetrics,
setup,
start,
end,
@ -27,6 +37,8 @@ export async function getLatencyChartsForBackend({
offset,
}: {
backendName: string;
spanName: string;
searchServiceDestinationMetrics: boolean;
setup: Setup;
start: number;
end: number;
@ -44,7 +56,11 @@ export async function getLatencyChartsForBackend({
const response = await apmEventClient.search('get_latency_for_backend', {
apm: {
events: [ProcessorEvent.metric],
events: [
getProcessorEventForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
],
},
body: {
size: 0,
@ -54,6 +70,10 @@ export async function getLatencyChartsForBackend({
...environmentQuery(environment),
...kqlQuery(kuery),
...rangeQuery(startWithOffset, endWithOffset),
...termQuery(SPAN_NAME, spanName || null),
...getDocumentTypeFilterForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
{ term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } },
],
},
@ -68,14 +88,22 @@ export async function getLatencyChartsForBackend({
aggs: {
latency_sum: {
sum: {
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM,
},
},
latency_count: {
sum: {
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
field: getLatencyFieldForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
},
},
...(searchServiceDestinationMetrics
? {
latency_count: {
sum: {
field: getDocCountFieldForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
},
},
}
: {}),
},
},
},
@ -86,7 +114,9 @@ export async function getLatencyChartsForBackend({
response.aggregations?.timeseries.buckets.map((bucket) => {
return {
x: bucket.key + offsetInMs,
y: (bucket.latency_sum.value ?? 0) / (bucket.latency_count.value ?? 0),
y:
(bucket.latency_sum.value ?? 0) /
(bucket.latency_count?.value ?? bucket.doc_count),
};
}) ?? []
);

View file

@ -5,32 +5,44 @@
* 2.0.
*/
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import {
kqlQuery,
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import {
SPAN_DESTINATION_SERVICE_RESOURCE,
SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
SPAN_NAME,
} from '../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../common/utils/environment_query';
import { ProcessorEvent } from '../../../common/processor_event';
import { Setup } from '../../lib/helpers/setup_request';
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
import { getBucketSize } from '../../lib/helpers/get_bucket_size';
import {
getDocCountFieldForServiceDestinationStatistics,
getDocumentTypeFilterForServiceDestinationStatistics,
getProcessorEventForServiceDestinationStatistics,
} from '../../lib/helpers/spans/get_is_using_service_destination_metrics';
export async function getThroughputChartsForBackend({
backendName,
spanName,
setup,
start,
end,
environment,
kuery,
searchServiceDestinationMetrics,
offset,
}: {
backendName: string;
spanName: string;
setup: Setup;
start: number;
end: number;
environment: string;
kuery: string;
searchServiceDestinationMetrics: boolean;
offset?: string;
}) {
const { apmEventClient } = setup;
@ -49,7 +61,11 @@ export async function getThroughputChartsForBackend({
const response = await apmEventClient.search('get_throughput_for_backend', {
apm: {
events: [ProcessorEvent.metric],
events: [
getProcessorEventForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
],
},
body: {
size: 0,
@ -59,6 +75,10 @@ export async function getThroughputChartsForBackend({
...environmentQuery(environment),
...kqlQuery(kuery),
...rangeQuery(startWithOffset, endWithOffset),
...termQuery(SPAN_NAME, spanName || null),
...getDocumentTypeFilterForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
{ term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } },
],
},
@ -74,7 +94,13 @@ export async function getThroughputChartsForBackend({
aggs: {
throughput: {
rate: {
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
...(searchServiceDestinationMetrics
? {
field: getDocCountFieldForServiceDestinationStatistics(
searchServiceDestinationMetrics
),
}
: {}),
unit: 'minute',
},
},

View file

@ -6,7 +6,7 @@
*/
import * as t from 'io-ts';
import { toNumberRt } from '@kbn/io-ts-utils';
import { toBooleanRt, toNumberRt } from '@kbn/io-ts-utils';
import { setupRequest } from '../../lib/helpers/setup_request';
import { environmentRt, kueryRt, rangeRt } from '../default_api_types';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
@ -278,7 +278,11 @@ const backendLatencyChartsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/backends/charts/latency',
params: t.type({
query: t.intersection([
t.type({ backendName: t.string }),
t.type({
backendName: t.string,
spanName: t.string,
searchServiceDestinationMetrics: toBooleanRt,
}),
rangeRt,
kueryRt,
environmentRt,
@ -296,12 +300,22 @@ const backendLatencyChartsRoute = createApmServerRoute({
}> => {
const setup = await setupRequest(resources);
const { params } = resources;
const { backendName, kuery, environment, offset, start, end } =
params.query;
const {
backendName,
searchServiceDestinationMetrics,
spanName,
kuery,
environment,
offset,
start,
end,
} = params.query;
const [currentTimeseries, comparisonTimeseries] = await Promise.all([
getLatencyChartsForBackend({
backendName,
spanName,
searchServiceDestinationMetrics,
setup,
start,
end,
@ -311,6 +325,8 @@ const backendLatencyChartsRoute = createApmServerRoute({
offset
? getLatencyChartsForBackend({
backendName,
spanName,
searchServiceDestinationMetrics,
setup,
start,
end,
@ -329,7 +345,11 @@ const backendThroughputChartsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/backends/charts/throughput',
params: t.type({
query: t.intersection([
t.type({ backendName: t.string }),
t.type({
backendName: t.string,
spanName: t.string,
searchServiceDestinationMetrics: toBooleanRt,
}),
rangeRt,
kueryRt,
environmentRt,
@ -347,27 +367,39 @@ const backendThroughputChartsRoute = createApmServerRoute({
}> => {
const setup = await setupRequest(resources);
const { params } = resources;
const { backendName, kuery, environment, offset, start, end } =
params.query;
const {
backendName,
searchServiceDestinationMetrics,
spanName,
kuery,
environment,
offset,
start,
end,
} = params.query;
const [currentTimeseries, comparisonTimeseries] = await Promise.all([
getThroughputChartsForBackend({
backendName,
spanName,
setup,
start,
end,
kuery,
environment,
searchServiceDestinationMetrics,
}),
offset
? getThroughputChartsForBackend({
backendName,
spanName,
setup,
start,
end,
kuery,
environment,
offset,
searchServiceDestinationMetrics,
})
: null,
]);
@ -380,7 +412,11 @@ const backendFailedTransactionRateChartsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/backends/charts/error_rate',
params: t.type({
query: t.intersection([
t.type({ backendName: t.string }),
t.type({
backendName: t.string,
spanName: t.string,
searchServiceDestinationMetrics: toBooleanRt,
}),
rangeRt,
kueryRt,
environmentRt,
@ -398,27 +434,39 @@ const backendFailedTransactionRateChartsRoute = createApmServerRoute({
}> => {
const setup = await setupRequest(resources);
const { params } = resources;
const { backendName, kuery, environment, offset, start, end } =
params.query;
const {
backendName,
spanName,
searchServiceDestinationMetrics,
kuery,
environment,
offset,
start,
end,
} = params.query;
const [currentTimeseries, comparisonTimeseries] = await Promise.all([
getErrorRateChartsForBackend({
backendName,
spanName,
setup,
start,
end,
kuery,
environment,
searchServiceDestinationMetrics,
}),
offset
? getErrorRateChartsForBackend({
backendName,
spanName,
setup,
start,
end,
kuery,
environment,
offset,
searchServiceDestinationMetrics,
})
: null,
]);

View file

@ -0,0 +1,48 @@
/*
* 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 { toBooleanRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import { TimeRangeMetadata } from '../../../common/time_range_metadata';
import { setupRequest } from '../../lib/helpers/setup_request';
import { getIsUsingServiceDestinationMetrics } from '../../lib/helpers/spans/get_is_using_service_destination_metrics';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import { kueryRt, rangeRt } from '../default_api_types';
export const timeRangeMetadataRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/time_range_metadata',
params: t.type({
query: t.intersection([
t.type({ useSpanName: toBooleanRt }),
kueryRt,
rangeRt,
]),
}),
options: {
tags: ['access:apm'],
},
handler: async (resources): Promise<TimeRangeMetadata> => {
const setup = await setupRequest(resources);
const {
query: { useSpanName, start, end, kuery },
} = resources.params;
const [isUsingServiceDestinationMetrics] = await Promise.all([
getIsUsingServiceDestinationMetrics({
setup,
useSpanName,
start,
end,
kuery,
}),
]);
return {
isUsingServiceDestinationMetrics,
};
},
});

View file

@ -0,0 +1,308 @@
/*
* 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 { sum } from 'lodash';
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
import { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { roundNumber } from '../../utils';
import { generateOperationData, generateOperationDataConfig } from './generate_operation_data';
import { SupertestReturnType } from '../../common/apm_api_supertest';
const {
ES_BULK_DURATION,
ES_BULK_RATE,
ES_SEARCH_DURATION,
ES_SEARCH_FAILURE_RATE,
ES_SEARCH_SUCCESS_RATE,
ES_SEARCH_UNKNOWN_RATE,
REDIS_SET_RATE,
} = generateOperationDataConfig;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
async function callApi<TMetricName extends 'latency' | 'throughput' | 'error_rate'>({
backendName,
searchServiceDestinationMetrics,
spanName = '',
metric,
kuery = '',
environment = ENVIRONMENT_ALL.value,
}: {
backendName: string;
searchServiceDestinationMetrics: boolean;
spanName?: string;
metric: TMetricName;
kuery?: string;
environment?: string;
}): Promise<SupertestReturnType<`GET /internal/apm/backends/charts/${TMetricName}`>> {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/backends/charts/${
metric as 'latency' | 'throughput' | 'error_rate'
}`,
params: {
query: {
backendName,
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
environment,
kuery,
offset: '',
spanName,
searchServiceDestinationMetrics,
},
},
});
}
function avg(coordinates: Coordinate[]) {
const values = coordinates
.filter((coord): coord is { x: number; y: number } => isFiniteNumber(coord.y))
.map((coord) => coord.y);
return roundNumber(sum(values) / values.length);
}
registry.when(
'Dependency metrics when data is not loaded',
{ config: 'basic', archives: [] },
() => {
it('handles empty state', async () => {
const { body, status } = await callApi({
backendName: 'elasticsearch',
metric: 'latency',
searchServiceDestinationMetrics: true,
});
expect(status).to.be(200);
expect(body.currentTimeseries.filter((val) => isFiniteNumber(val.y))).to.empty();
expect(
(body.comparisonTimeseries || [])?.filter((val) => isFiniteNumber(val.y))
).to.empty();
});
}
);
registry.when(
'Dependency metrics when data is loaded',
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
() => {
before(async () => {
await generateOperationData({
synthtraceEsClient,
start,
end,
});
});
describe('without spanName', () => {
describe('without a kuery or environment', () => {
it('returns the correct latency', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'latency',
});
const searchRate =
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
const bulkRate = ES_BULK_RATE;
expect(avg(response.body.currentTimeseries)).to.eql(
roundNumber(
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
(searchRate + bulkRate)) *
1000
)
);
});
it('returns the correct throughput', async () => {
const response = await callApi({
backendName: 'redis',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'throughput',
});
expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE);
});
it('returns the correct failure rate', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'error_rate',
});
const expectedErrorRate =
ES_SEARCH_FAILURE_RATE / (ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE);
expect(avg(response.body.currentTimeseries)).to.eql(expectedErrorRate);
});
});
describe('with a kuery', () => {
it('returns the correct latency', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'latency',
kuery: `event.outcome:unknown`,
});
const searchRate = ES_SEARCH_UNKNOWN_RATE;
const bulkRate = ES_BULK_RATE;
expect(avg(response.body.currentTimeseries)).to.eql(
roundNumber(
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
(searchRate + bulkRate)) *
1000
)
);
});
it('returns the correct throughput', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'throughput',
kuery: `event.outcome:unknown`,
});
const searchRate = ES_SEARCH_UNKNOWN_RATE;
const bulkRate = ES_BULK_RATE;
expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate));
});
it('returns the correct failure rate', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'error_rate',
kuery: 'event.outcome:success',
});
expect(avg(response.body.currentTimeseries)).to.eql(0);
});
});
describe('with an environment', () => {
it('returns the correct latency', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'latency',
environment: 'production',
});
const searchRate = ES_SEARCH_UNKNOWN_RATE;
const bulkRate = 0;
expect(avg(response.body.currentTimeseries)).to.eql(
roundNumber(
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
(searchRate + bulkRate)) *
1000
)
);
});
it('returns the correct throughput', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'throughput',
environment: 'production',
});
const searchRate =
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
const bulkRate = 0;
expect(avg(response.body.currentTimeseries)).to.eql(roundNumber(searchRate + bulkRate));
});
it('returns the correct failure rate', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: true,
spanName: '',
metric: 'error_rate',
environment: 'development',
});
expect(avg(response.body.currentTimeseries)).to.eql(null);
});
});
});
describe('with spanName', () => {
it('returns the correct latency', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: false,
spanName: '/_search',
metric: 'latency',
});
const searchRate =
ES_SEARCH_FAILURE_RATE + ES_SEARCH_SUCCESS_RATE + ES_SEARCH_UNKNOWN_RATE;
const bulkRate = 0;
expect(avg(response.body.currentTimeseries)).to.eql(
roundNumber(
((ES_SEARCH_DURATION * searchRate + ES_BULK_DURATION * bulkRate) /
(searchRate + bulkRate)) *
1000
)
);
});
it('returns the correct throughput', async () => {
const response = await callApi({
backendName: 'redis',
searchServiceDestinationMetrics: false,
spanName: 'SET',
metric: 'throughput',
});
expect(avg(response.body.currentTimeseries)).to.eql(REDIS_SET_RATE);
});
it('returns the correct failure rate', async () => {
const response = await callApi({
backendName: 'elasticsearch',
searchServiceDestinationMetrics: false,
spanName: '/_bulk',
metric: 'error_rate',
});
expect(avg(response.body.currentTimeseries)).to.eql(null);
});
});
after(() => synthtraceEsClient.clean());
}
);
}

View file

@ -0,0 +1,84 @@
/*
* 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 { apm, timerange } from '@elastic/apm-synthtrace';
import { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
export const generateOperationDataConfig = {
ES_SEARCH_DURATION: 100,
ES_SEARCH_UNKNOWN_RATE: 5,
ES_BULK_RATE: 20,
ES_SEARCH_SUCCESS_RATE: 4,
ES_SEARCH_FAILURE_RATE: 1,
ES_BULK_DURATION: 1000,
REDIS_SET_RATE: 10,
REDIS_SET_DURATION: 10,
};
export async function generateOperationData({
start,
end,
synthtraceEsClient,
}: {
start: number;
end: number;
synthtraceEsClient: ApmSynthtraceEsClient;
}) {
const synthGoInstance = apm.service('synth-go', 'production', 'go').instance('instance-a');
const synthJavaInstance = apm.service('synth-java', 'development', 'java').instance('instance-a');
const interval = timerange(start, end).interval('1m');
return await synthtraceEsClient.index([
interval
.rate(generateOperationDataConfig.ES_SEARCH_UNKNOWN_RATE)
.generator((timestamp) =>
synthGoInstance
.span('/_search', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.duration(generateOperationDataConfig.ES_SEARCH_DURATION)
),
interval
.rate(generateOperationDataConfig.ES_SEARCH_SUCCESS_RATE)
.generator((timestamp) =>
synthGoInstance
.span('/_search', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.success()
.duration(generateOperationDataConfig.ES_SEARCH_DURATION)
),
interval
.rate(generateOperationDataConfig.ES_SEARCH_FAILURE_RATE)
.generator((timestamp) =>
synthGoInstance
.span('/_search', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.failure()
.duration(generateOperationDataConfig.ES_SEARCH_DURATION)
),
interval
.rate(generateOperationDataConfig.ES_BULK_RATE)
.generator((timestamp) =>
synthJavaInstance
.span('/_bulk', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.duration(generateOperationDataConfig.ES_BULK_DURATION)
),
interval
.rate(generateOperationDataConfig.REDIS_SET_RATE)
.generator((timestamp) =>
synthJavaInstance
.span('SET', 'db', 'redis')
.destination('redis')
.timestamp(timestamp)
.duration(generateOperationDataConfig.REDIS_SET_DURATION)
),
]);
}

View file

@ -6,14 +6,25 @@
*/
import expect from '@kbn/expect';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { apm, timerange } from '@elastic/apm-synthtrace';
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
import { ValuesType } from 'utility-types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { roundNumber } from '../../utils';
import { generateOperationData, generateOperationDataConfig } from './generate_operation_data';
type TopOperations = APIReturnType<'GET /internal/apm/backends/operations'>['operations'];
const {
ES_BULK_DURATION,
ES_BULK_RATE,
ES_SEARCH_DURATION,
ES_SEARCH_FAILURE_RATE,
ES_SEARCH_SUCCESS_RATE,
ES_SEARCH_UNKNOWN_RATE,
REDIS_SET_DURATION,
REDIS_SET_RATE,
} = generateOperationDataConfig;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
@ -47,76 +58,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
.then(({ body }) => body.operations);
}
const ES_SEARCH_DURATION = 100;
const ES_SEARCH_UNKNOWN_RATE = 5;
const ES_SEARCH_SUCCESS_RATE = 4;
const ES_SEARCH_FAILURE_RATE = 1;
const ES_BULK_RATE = 20;
const ES_BULK_DURATION = 1000;
const REDIS_SET_RATE = 10;
const REDIS_SET_DURATION = 10;
async function generateData() {
const synthGoInstance = apm.service('synth-go', 'production', 'go').instance('instance-a');
const synthJavaInstance = apm
.service('synth-java', 'development', 'java')
.instance('instance-a');
const interval = timerange(start, end).interval('1m');
return await synthtraceEsClient.index([
interval
.rate(ES_SEARCH_UNKNOWN_RATE)
.generator((timestamp) =>
synthGoInstance
.span('/_search', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.duration(ES_SEARCH_DURATION)
),
interval
.rate(ES_SEARCH_SUCCESS_RATE)
.generator((timestamp) =>
synthGoInstance
.span('/_search', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.success()
.duration(ES_SEARCH_DURATION)
),
interval
.rate(ES_SEARCH_FAILURE_RATE)
.generator((timestamp) =>
synthGoInstance
.span('/_search', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.failure()
.duration(ES_SEARCH_DURATION)
),
interval
.rate(ES_BULK_RATE)
.generator((timestamp) =>
synthJavaInstance
.span('/_bulk', 'db', 'elasticsearch')
.destination('elasticsearch')
.timestamp(timestamp)
.duration(ES_BULK_DURATION)
),
interval
.rate(REDIS_SET_RATE)
.generator((timestamp) =>
synthJavaInstance
.span('SET', 'db', 'redis')
.destination('redis')
.timestamp(timestamp)
.duration(REDIS_SET_DURATION)
),
]);
}
registry.when('Top operations when data is not loaded', { config: 'basic', archives: [] }, () => {
it('handles empty state', async () => {
const operations = await callApi({ backendName: 'elasticsearch' });
@ -128,7 +69,13 @@ export default function ApiTest({ getService }: FtrProviderContext) {
'Top operations when data is generated',
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
() => {
before(() => generateData());
before(() =>
generateOperationData({
synthtraceEsClient,
start,
end,
})
);
after(() => synthtraceEsClient.clean());

View file

@ -43,6 +43,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
query: {
...commonQuery,
backendName: overrides?.backendName || 'elasticsearch',
spanName: '',
searchServiceDestinationMetrics: false,
kuery: '',
},
},