diff --git a/x-pack/plugins/apm/common/connections.ts b/x-pack/plugins/apm/common/connections.ts
index aa3863374337c..1fde50ce5489e 100644
--- a/x-pack/plugins/apm/common/connections.ts
+++ b/x-pack/plugins/apm/common/connections.ts
@@ -47,6 +47,10 @@ export interface ConnectionStatsItem {
value: number | null;
timeseries: Coordinate[];
};
+ totalTime: {
+ value: number | null;
+ timeseries: Coordinate[];
+ };
};
}
diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx
index e4b3d48992efa..c89526b332084 100644
--- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx
@@ -51,11 +51,11 @@ export function BackendInventoryDependenciesTable() {
return callApmApi({
endpoint: 'GET /api/apm/backends/top_backends',
params: {
- query: { start, end, environment, numBuckets: 20, offset },
+ query: { start, end, environment, numBuckets: 20, offset, kuery },
},
});
},
- [start, end, environment, offset]
+ [start, end, environment, offset, kuery]
);
const dependencies =
diff --git a/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx b/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx
index b89586b42fd6b..1488c4773359c 100644
--- a/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx
@@ -4,8 +4,35 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { EuiSpacer } from '@elastic/eui';
+import { EuiTitle } from '@elastic/eui';
+import { EuiPanel } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import React from 'react';
+import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
+import { ServiceOverviewDependenciesTable } from '../service_overview/service_overview_dependencies_table';
+import { ServiceDependenciesBreakdownChart } from './service_dependencies_breakdown_chart';
export function ServiceDependencies() {
- return <>>;
+ return (
+ <>
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.serviceDependencies.breakdownChartTitle',
+ {
+ defaultMessage: 'Time spent by dependency',
+ }
+ )}
+
+
+
+
+
+
+
+ >
+ );
}
diff --git a/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx
new file mode 100644
index 0000000000000..a33b0db7c4baf
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { getVizColorForIndex } from '../../../../common/viz_colors';
+import { Coordinate, TimeSeries } from '../../../../typings/timeseries';
+import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
+import { useApmParams } from '../../../hooks/use_apm_params';
+import { useFetcher } from '../../../hooks/use_fetcher';
+import { useTimeRange } from '../../../hooks/use_time_range';
+import { BreakdownChart } from '../../shared/charts/breakdown_chart';
+
+export function ServiceDependenciesBreakdownChart({
+ height,
+}: {
+ height: number;
+}) {
+ const { start, end } = useTimeRange();
+ const { serviceName } = useApmServiceContext();
+
+ const {
+ query: { kuery, environment },
+ } = useApmParams('/services/:serviceName/dependencies');
+
+ const { data, status } = useFetcher(
+ (callApmApi) => {
+ return callApmApi({
+ endpoint: 'GET /api/apm/services/{serviceName}/dependencies/breakdown',
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ start,
+ end,
+ kuery,
+ environment,
+ },
+ },
+ });
+ },
+ [serviceName, start, end, kuery, environment]
+ );
+
+ const timeseries: Array> =
+ data?.breakdown.map((item, index) => {
+ return {
+ title: item.title,
+ data: item.data,
+ type: 'area',
+ color: getVizColorForIndex(index),
+ };
+ }) ?? [];
+
+ return (
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx
index 08d554b9a54e8..d9df9acf9ff65 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx
@@ -34,7 +34,7 @@ export function ServiceOverviewDependenciesTable() {
const {
query,
query: { kuery, rangeFrom, rangeTo },
- } = useApmParams('/services/:serviceName/overview');
+ } = useApmParams('/services/:serviceName/*');
const { offset } = getTimeRangeComparison({
start,
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx
similarity index 88%
rename from x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx
rename to x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx
index 4fdfbff7917d1..f39c39113fedc 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx
@@ -16,19 +16,21 @@ import {
Position,
ScaleType,
Settings,
+ TickFormatter,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React from 'react';
import { useHistory } from 'react-router-dom';
+import { Annotation } from '../../../../../common/annotations';
import { useChartTheme } from '../../../../../../observability/public';
import {
asAbsoluteDateTime,
+ asDuration,
asPercent,
} from '../../../../../common/utils/formatters';
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
-import { useAnnotationsContext } from '../../../../context/annotations/use_annotations_context';
import { useChartPointerEventContext } from '../../../../context/chart_pointer_event/use_chart_pointer_event_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
@@ -41,17 +43,23 @@ interface Props {
fetchStatus: FETCH_STATUS;
height?: number;
showAnnotations: boolean;
+ annotations: Annotation[];
timeseries?: Array>;
+ yAxisType: 'duration' | 'percentage';
}
-export function TransactionBreakdownChartContents({
+const asPercentBound = (y: number | null) => asPercent(y, 1);
+const asDurationBound = (y: number | null) => asDuration(y);
+
+export function BreakdownChart({
fetchStatus,
height = unit * 16,
showAnnotations,
+ annotations,
timeseries,
+ yAxisType,
}: Props) {
const history = useHistory();
- const { annotations } = useAnnotationsContext();
const chartTheme = useChartTheme();
const { chartRef, setPointerEvent } = useChartPointerEventContext();
@@ -68,6 +76,9 @@ export function TransactionBreakdownChartContents({
const isEmpty = isTimeseriesEmpty(timeseries);
+ const yTickFormat: TickFormatter =
+ yAxisType === 'duration' ? asDurationBound : asPercentBound;
+
return (
@@ -98,7 +109,7 @@ export function TransactionBreakdownChartContents({
id="y-axis"
ticks={3}
position={Position.Left}
- tickFormat={(y: number) => asPercent(y ?? 0, 1)}
+ tickFormat={yTickFormat}
/>
{showAnnotations && (
@@ -133,7 +144,9 @@ export function TransactionBreakdownChartContents({
yAccessors={['y']}
data={serie.data}
stackAccessors={['x']}
- stackMode={'percentage'}
+ stackMode={
+ yAxisType === 'percentage' ? 'percentage' : undefined
+ }
color={serie.areaColor}
curve={CurveType.CURVE_MONOTONE_X}
/>
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx
index 40c5e39589fb1..0e2b1e185f9d9 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx
@@ -8,8 +8,9 @@
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
+import { useAnnotationsContext } from '../../../../context/annotations/use_annotations_context';
import { useTransactionBreakdown } from './use_transaction_breakdown';
-import { TransactionBreakdownChartContents } from './transaction_breakdown_chart_contents';
+import { BreakdownChart } from '../breakdown_chart';
export function TransactionBreakdownChart({
height,
@@ -19,6 +20,7 @@ export function TransactionBreakdownChart({
showAnnotations?: boolean;
}) {
const { data, status } = useTransactionBreakdown();
+ const { annotations } = useAnnotationsContext();
const { timeseries } = data;
return (
@@ -34,11 +36,13 @@ export function TransactionBreakdownChart({
-
diff --git a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts
index 4fbd521ea4443..ae8a6db903d8a 100644
--- a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts
+++ b/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
-import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/elasticsearch_fieldnames';
+import {
+ SPAN_DESTINATION_SERVICE_RESOURCE,
+ SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
+} from '../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../common/utils/environment_query';
import { kqlQuery, rangeQuery } from '../../../../observability/server';
import { ProcessorEvent } from '../../../common/processor_event';
@@ -63,7 +66,10 @@ export async function getThroughputChartsForBackend({
}),
aggs: {
throughput: {
- rate: {},
+ rate: {
+ field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
+ unit: 'minute',
+ },
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts b/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts
index 2f9af0bafd37c..9b361774b8a9f 100644
--- a/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts
+++ b/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts
@@ -5,10 +5,11 @@
* 2.0.
*/
+import { kqlQuery } from '../../../../observability/server';
+import { NodeType } from '../../../common/connections';
import { environmentQuery } from '../../../common/utils/environment_query';
import { getConnectionStats } from '../connections/get_connection_stats';
import { getConnectionStatsItemsWithRelativeImpact } from '../connections/get_connection_stats/get_connection_stats_items_with_relative_impact';
-import { NodeType } from '../../../common/connections';
import { Setup } from '../helpers/setup_request';
export async function getTopBackends({
@@ -18,6 +19,7 @@ export async function getTopBackends({
numBuckets,
environment,
offset,
+ kuery,
}: {
setup: Setup;
start: number;
@@ -25,13 +27,14 @@ export async function getTopBackends({
numBuckets: number;
environment?: string;
offset?: string;
+ kuery?: string;
}) {
const statsItems = await getConnectionStats({
setup,
start,
end,
numBuckets,
- filter: [...environmentQuery(environment)],
+ filter: [...environmentQuery(environment), ...kqlQuery(kuery)],
offset,
collapseBy: 'downstream',
});
diff --git a/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_stats.ts b/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_stats.ts
index 94a23f6218e5d..4f48f3388c017 100644
--- a/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_stats.ts
+++ b/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_stats.ts
@@ -119,6 +119,16 @@ export const getStats = async ({
},
},
},
+ total_latency_sum: {
+ sum: {
+ field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM,
+ },
+ },
+ total_latency_count: {
+ sum: {
+ field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
+ },
+ },
timeseries: {
date_histogram: {
field: '@timestamp',
@@ -126,6 +136,7 @@ export const getStats = async ({
start: startWithOffset,
end: endWithOffset,
numBuckets,
+ minBucketSize: 60,
}).intervalString,
extended_bounds: {
min: startWithOffset,
diff --git a/x-pack/plugins/apm/server/lib/connections/get_connection_stats/index.ts b/x-pack/plugins/apm/server/lib/connections/get_connection_stats/index.ts
index 37fcd9877327b..03b94defda6dd 100644
--- a/x-pack/plugins/apm/server/lib/connections/get_connection_stats/index.ts
+++ b/x-pack/plugins/apm/server/lib/connections/get_connection_stats/index.ts
@@ -113,6 +113,13 @@ export function getConnectionStats({
y: point.count > 0 ? point.latency_sum / point.count : null,
})),
},
+ totalTime: {
+ value: mergedStats.value.latency_sum,
+ timeseries: mergedStats.timeseries.map((point) => ({
+ x: point.x,
+ y: point.latency_sum,
+ })),
+ },
throughput: {
value:
mergedStats.value.count > 0
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies_breakdown.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies_breakdown.ts
new file mode 100644
index 0000000000000..8ebf6b7e017d4
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies_breakdown.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { sortBy, take } from 'lodash';
+import { getNodeName } from '../../../common/connections';
+import { kqlQuery } from '../../../../observability/server';
+import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames';
+import { environmentQuery } from '../../../common/utils/environment_query';
+import { Setup } from '../helpers/setup_request';
+import { getConnectionStats } from '../connections/get_connection_stats';
+
+export async function getServiceDependenciesBreakdown({
+ setup,
+ start,
+ end,
+ serviceName,
+ environment,
+ kuery,
+}: {
+ setup: Setup;
+ start: number;
+ end: number;
+ serviceName: string;
+ environment?: string;
+ kuery?: string;
+}) {
+ const items = await getConnectionStats({
+ setup,
+ start,
+ end,
+ numBuckets: 100,
+ collapseBy: 'downstream',
+ filter: [
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ { term: { [SERVICE_NAME]: serviceName } },
+ ],
+ });
+
+ return take(
+ sortBy(items, (item) => item.stats.totalTime ?? 0).reverse(),
+ 20
+ ).map((item) => {
+ const { stats, location } = item;
+
+ return {
+ title: getNodeName(location),
+ data: stats.totalTime.timeseries,
+ };
+ });
+}
diff --git a/x-pack/plugins/apm/server/routes/backends.ts b/x-pack/plugins/apm/server/routes/backends.ts
index c738e9aa64007..e673770dbbc1a 100644
--- a/x-pack/plugins/apm/server/routes/backends.ts
+++ b/x-pack/plugins/apm/server/routes/backends.ts
@@ -25,7 +25,7 @@ const topBackendsRoute = createApmServerRoute({
query: t.intersection([rangeRt, t.type({ numBuckets: toNumberRt })]),
}),
t.partial({
- query: t.intersection([environmentRt, offsetRt]),
+ query: t.intersection([environmentRt, offsetRt, kueryRt]),
}),
]),
options: {
@@ -35,9 +35,9 @@ const topBackendsRoute = createApmServerRoute({
const setup = await setupRequest(resources);
const { start, end } = setup;
- const { environment, offset, numBuckets } = resources.params.query;
+ const { environment, offset, numBuckets, kuery } = resources.params.query;
- const opts = { setup, start, end, numBuckets, environment };
+ const opts = { setup, start, end, numBuckets, environment, kuery };
const [currentBackends, previousBackends] = await Promise.all([
getTopBackends(opts),
diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts
index 87eac9374fd02..f5156fe85fbf5 100644
--- a/x-pack/plugins/apm/server/routes/services.ts
+++ b/x-pack/plugins/apm/server/routes/services.ts
@@ -42,6 +42,7 @@ import {
} from './default_api_types';
import { offsetPreviousPeriodCoordinates } from '../../common/utils/offset_previous_period_coordinate';
import { getServicesDetailedStatistics } from '../lib/services/get_services_detailed_statistics';
+import { getServiceDependenciesBreakdown } from '../lib/services/get_service_dependencies_breakdown';
const servicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services',
@@ -692,6 +693,38 @@ export const serviceDependenciesRoute = createApmServerRoute({
},
});
+export const serviceDependenciesBreakdownRoute = createApmServerRoute({
+ endpoint: 'GET /api/apm/services/{serviceName}/dependencies/breakdown',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.intersection([environmentRt, rangeRt, kueryRt]),
+ }),
+ options: {
+ tags: ['access:apm'],
+ },
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const { environment, start, end, kuery } = params.query;
+
+ const breakdown = await getServiceDependenciesBreakdown({
+ setup,
+ start,
+ end,
+ serviceName,
+ environment,
+ kuery,
+ });
+
+ return {
+ breakdown,
+ };
+ },
+});
+
const serviceProfilingTimelineRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline',
params: t.type({
@@ -822,6 +855,7 @@ export const serviceRouteRepository = createApmServerRouteRepository()
.add(serviceInstancesMainStatisticsRoute)
.add(serviceInstancesDetailedStatisticsRoute)
.add(serviceDependenciesRoute)
+ .add(serviceDependenciesBreakdownRoute)
.add(serviceProfilingTimelineRoute)
.add(serviceProfilingStatisticsRoute)
.add(serviceAlertsRoute);
diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts
index 4f10ee6ff6bd8..942378477f04c 100644
--- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts
+++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts
@@ -357,6 +357,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
'errorRate.timeseries',
'throughput.timeseries',
'latency.timeseries',
+ 'totalTime.timeseries',
])
).toMatchInline(`
Object {
@@ -370,6 +371,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
"throughput": Object {
"value": 1.83333333333333,
},
+ "totalTime": Object {
+ "value": 61439716,
+ },
}
`);
});
@@ -467,8 +471,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
response.body.serviceDependencies.map((item) => ({
name: getName(item.location),
impact: item.currentStats.impact,
- latency: item.currentStats.latency.value,
- throughput: item.currentStats.throughput.value,
})),
'name'
);
@@ -477,27 +479,50 @@ export default function ApiTest({ getService }: FtrProviderContext) {
Array [
Object {
"impact": 0,
- "latency": 9496.32291666667,
"name": "elasticsearch",
- "throughput": 3.2,
},
Object {
"impact": 100,
- "latency": 1117085.74545455,
"name": "opbeans-dotnet",
- "throughput": 1.83333333333333,
},
Object {
"impact": 71.0403531954737,
- "latency": 27826.9968314322,
"name": "postgresql",
- "throughput": 52.6,
},
Object {
"impact": 1.41447268043525,
- "latency": 1468.27242524917,
"name": "redis",
- "throughput": 40.1333333333333,
+ },
+ ]
+ `);
+ });
+
+ it('returns the right totalTime values', () => {
+ const totalTimeValues = sortBy(
+ response.body.serviceDependencies.map((item) => ({
+ name: getName(item.location),
+ totalTime: item.currentStats.totalTime.value,
+ })),
+ 'name'
+ );
+
+ expectSnapshot(totalTimeValues).toMatchInline(`
+ Array [
+ Object {
+ "name": "elasticsearch",
+ "totalTime": 911647,
+ },
+ Object {
+ "name": "opbeans-dotnet",
+ "totalTime": 61439716,
+ },
+ Object {
+ "name": "postgresql",
+ "totalTime": 43911001,
+ },
+ Object {
+ "name": "redis",
+ "totalTime": 1767800,
},
]
`);