mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
3afe960302
commit
7604d3beef
30 changed files with 1279 additions and 283 deletions
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
10
x-pack/plugins/apm/common/time_range_metadata.ts
Normal file
10
x-pack/plugins/apm/common/time_range_metadata.ts
Normal 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;
|
||||
}
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 />,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
|
|
@ -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(
|
|
@ -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(
|
|
@ -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'
|
||||
>;
|
|
@ -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(
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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());
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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)
|
||||
),
|
||||
]);
|
||||
}
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -43,6 +43,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
query: {
|
||||
...commonQuery,
|
||||
backendName: overrides?.backendName || 'elasticsearch',
|
||||
spanName: '',
|
||||
searchServiceDestinationMetrics: false,
|
||||
kuery: '',
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue