From 4e42e5dc4438d0b9835e40b756afba47867e40f3 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 4 Aug 2021 12:21:34 +0200 Subject: [PATCH] [APM] Add throughput, error rate charts to backend detail page (#107379) (#107618) --- .../backend_detail_dependencies_table.tsx | 4 +- .../backend_error_rate_chart.tsx | 100 ++++++++++++++++ .../backend_latency_chart.tsx | 4 +- .../backend_throughput_chart.tsx | 96 +++++++++++++++ .../app/backend_detail_overview/index.tsx | 50 +++++++- .../templates/backend_detail_template.tsx | 50 ++++++++ .../get_error_rate_charts_for_backend.ts | 98 ++++++++++++++++ .../lib/backends/get_metadata_for_backend.ts | 4 +- .../get_throughput_charts_for_backend.ts | 82 +++++++++++++ .../get_upstream_services_for_backend.ts | 4 + x-pack/plugins/apm/server/routes/backends.ts | 110 +++++++++++++++++- 11 files changed, 586 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx create mode 100644 x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx create mode 100644 x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx create mode 100644 x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts create mode 100644 x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx index e254f34d1105f..425506a3e035a 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx @@ -58,11 +58,11 @@ export function BackendDetailDependenciesTable() { path: { backendName, }, - query: { start, end, environment, numBuckets: 20, offset }, + query: { start, end, environment, numBuckets: 20, offset, kuery }, }, }); }, - [start, end, environment, offset, backendName] + [start, end, environment, offset, backendName, kuery] ); const dependencies = diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx new file mode 100644 index 0000000000000..8dc47dc6549fc --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx @@ -0,0 +1,100 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { asPercent } from '../../../../common/utils/formatters'; +import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useComparison } from '../../../hooks/use_comparison'; +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 { useTheme } from '../../../hooks/use_theme'; + +function yLabelFormat(y?: number | null) { + return asPercent(y || 0, 1); +} + +export function BackendErrorRateChart({ height }: { height: number }) { + const { backendName } = useApmBackendContext(); + + const theme = useTheme(); + + const { start, end } = useTimeRange(); + + const { + urlParams: { kuery, environment }, + } = useUrlParams(); + + const { offset, comparisonChartTheme } = useComparison(); + + const { data, status } = useFetcher( + (callApmApi) => { + if (!start || !end) { + return; + } + + return callApmApi({ + endpoint: 'GET /api/apm/backends/{backendName}/charts/error_rate', + params: { + path: { + backendName, + }, + query: { + start, + end, + offset, + kuery, + environment, + }, + }, + }); + }, + [backendName, start, end, offset, kuery, environment] + ); + + const timeseries = useMemo(() => { + const specs: Array> = []; + + if (data?.currentTimeseries) { + specs.push({ + data: data.currentTimeseries, + type: 'linemark', + color: theme.eui.euiColorVis7, + title: i18n.translate('xpack.apm.backendErrorRateChart.chartTitle', { + defaultMessage: 'Error rate', + }), + }); + } + + if (data?.comparisonTimeseries) { + specs.push({ + data: data.comparisonTimeseries, + type: 'area', + color: theme.eui.euiColorMediumShade, + title: i18n.translate( + 'xpack.apm.backendErrorRateChart.previousPeriodLabel', + { defaultMessage: 'Previous period' } + ), + }); + } + + return specs; + }, [data, theme.eui.euiColorVis7, theme.eui.euiColorMediumShade]); + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx index b1e58a089c8cd..0f855edfae770 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx @@ -65,7 +65,7 @@ export function BackendLatencyChart({ height }: { height: number }) { specs.push({ data: data.currentTimeseries, type: 'linemark', - color: theme.eui.euiColorVis0, + color: theme.eui.euiColorVis1, title: i18n.translate('xpack.apm.backendLatencyChart.chartTitle', { defaultMessage: 'Latency', }), @@ -85,7 +85,7 @@ export function BackendLatencyChart({ height }: { height: number }) { } return specs; - }, [data, theme.eui.euiColorVis0, theme.eui.euiColorMediumShade]); + }, [data, theme.eui.euiColorVis1, theme.eui.euiColorMediumShade]); const maxY = getMaxY(timeseries); const latencyFormatter = getDurationFormatter(maxY); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx new file mode 100644 index 0000000000000..0b962742051b4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx @@ -0,0 +1,96 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { asTransactionRate } from '../../../../common/utils/formatters'; +import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useComparison } from '../../../hooks/use_comparison'; +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 { useTheme } from '../../../hooks/use_theme'; + +export function BackendThroughputChart({ height }: { height: number }) { + const { backendName } = useApmBackendContext(); + + const theme = useTheme(); + + const { start, end } = useTimeRange(); + + const { + urlParams: { kuery, environment }, + } = useUrlParams(); + + const { offset, comparisonChartTheme } = useComparison(); + + const { data, status } = useFetcher( + (callApmApi) => { + if (!start || !end) { + return; + } + + return callApmApi({ + endpoint: 'GET /api/apm/backends/{backendName}/charts/throughput', + params: { + path: { + backendName, + }, + query: { + start, + end, + offset, + kuery, + environment, + }, + }, + }); + }, + [backendName, start, end, offset, kuery, environment] + ); + + const timeseries = useMemo(() => { + const specs: Array> = []; + + if (data?.currentTimeseries) { + specs.push({ + data: data.currentTimeseries, + type: 'linemark', + color: theme.eui.euiColorVis0, + title: i18n.translate('xpack.apm.backendThroughputChart.chartTitle', { + defaultMessage: 'Throughput', + }), + }); + } + + if (data?.comparisonTimeseries) { + specs.push({ + data: data.comparisonTimeseries, + type: 'area', + color: theme.eui.euiColorMediumShade, + title: i18n.translate( + 'xpack.apm.backendThroughputChart.previousPeriodLabel', + { defaultMessage: 'Previous period' } + ), + }); + } + + return specs; + }, [data, theme.eui.euiColorVis0, theme.eui.euiColorMediumShade]); + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 7773506bc1eda..cef0b4e410c38 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -9,16 +9,20 @@ 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 { ApmBackendContextProvider } from '../../../context/apm_backend/apm_backend_context'; 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 { ApmMainTemplate } from '../../routing/templates/apm_main_template'; import { SearchBar } from '../../shared/search_bar'; import { BackendLatencyChart } from './backend_latency_chart'; import { BackendInventoryTitle } from '../../routing/home'; import { BackendDetailDependenciesTable } from './backend_detail_dependencies_table'; +import { BackendThroughputChart } from './backend_throughput_chart'; +import { BackendErrorRateChart } from './backend_error_rate_chart'; +import { BackendDetailTemplate } from '../../routing/templates/backend_detail_template'; export function BackendDetailOverview() { const { @@ -43,21 +47,55 @@ export function BackendDetailOverview() { ]); return ( - - + + - + + +

+ {i18n.translate( + 'xpack.apm.backendDetailLatencyChartTitle', + { defaultMessage: 'Latency' } + )} +

+
+ + + +

+ {i18n.translate( + 'xpack.apm.backendDetailThroughputChartTitle', + { defaultMessage: 'Throughput' } + )} +

+
+ +
+
+ + + +

+ {i18n.translate( + 'xpack.apm.backendDetailErrorRateChartTitle', + { defaultMessage: 'Error rate' } + )} +

+
+ +
+
-
-
+ + ); } diff --git a/x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx new file mode 100644 index 0000000000000..27eb16a0221b7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/templates/backend_detail_template.tsx @@ -0,0 +1,50 @@ +/* + * 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, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context'; +import { ApmMainTemplate } from './apm_main_template'; +import { SpanIcon } from '../../shared/span_icon'; + +interface Props { + title: string; + children: React.ReactNode; +} + +export function BackendDetailTemplate({ title, children }: Props) { + const { + backendName, + metadata: { data }, + } = useApmBackendContext(); + + const metadata = data?.metadata; + + return ( + + + +

{backendName}

+
+
+ + + + + ), + }} + > + {children} +
+ ); +} diff --git a/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts new file mode 100644 index 0000000000000..736b146d3d2ab --- /dev/null +++ b/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts @@ -0,0 +1,98 @@ +/* + * 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 { EventOutcome } from '../../../common/event_outcome'; +import { + EVENT_OUTCOME, + SPAN_DESTINATION_SERVICE_RESOURCE, +} from '../../../common/elasticsearch_fieldnames'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { Setup } from '../helpers/setup_request'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; + +export async function getErrorRateChartsForBackend({ + backendName, + setup, + start, + end, + environment, + kuery, + offset, +}: { + backendName: string; + setup: Setup; + start: number; + end: number; + environment?: string; + kuery?: string; + offset?: string; +}) { + const { apmEventClient } = setup; + + const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({ + start, + end, + offset, + }); + + const response = await apmEventClient.search('get_error_rate_for_backend', { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...rangeQuery(startWithOffset, endWithOffset), + { term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } }, + { + terms: { + [EVENT_OUTCOME]: [EventOutcome.success, EventOutcome.failure], + }, + }, + ], + }, + }, + aggs: { + timeseries: { + date_histogram: getMetricsDateHistogramParams({ + start: startWithOffset, + end: endWithOffset, + metricsInterval: 60, + }), + aggs: { + failures: { + filter: { + term: { + [EVENT_OUTCOME]: EventOutcome.failure, + }, + }, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.timeseries.buckets.map((bucket) => { + const totalCount = bucket.doc_count; + const failureCount = bucket.failures.doc_count; + + return { + x: bucket.key + offsetInMs, + y: failureCount / totalCount, + }; + }) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/backends/get_metadata_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_metadata_for_backend.ts index 9e49fd6d6c390..912014602dd13 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_metadata_for_backend.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_metadata_for_backend.ts @@ -51,7 +51,7 @@ export async function getMetadataForBackend({ const sample = maybe(sampleResponse.hits.hits[0])?._source; return { - 'span.type': sample?.span.type, - 'span.subtype': sample?.span.subtype, + spanType: sample?.span.type, + spanSubtype: sample?.span.subtype, }; } 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 new file mode 100644 index 0000000000000..4fbd521ea4443 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts @@ -0,0 +1,82 @@ +/* + * 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 { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/elasticsearch_fieldnames'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { Setup } from '../helpers/setup_request'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; + +export async function getThroughputChartsForBackend({ + backendName, + setup, + start, + end, + environment, + kuery, + offset, +}: { + backendName: string; + setup: Setup; + start: number; + end: number; + environment?: string; + kuery?: string; + offset?: string; +}) { + const { apmEventClient } = setup; + + const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({ + start, + end, + offset, + }); + + const response = await apmEventClient.search('get_throughput_for_backend', { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...rangeQuery(startWithOffset, endWithOffset), + { term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } }, + ], + }, + }, + aggs: { + timeseries: { + date_histogram: getMetricsDateHistogramParams({ + start: startWithOffset, + end: endWithOffset, + metricsInterval: 60, + }), + aggs: { + throughput: { + rate: {}, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.timeseries.buckets.map((bucket) => { + return { + x: bucket.key + offsetInMs, + y: bucket.throughput.value, + }; + }) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts index 2105f389ef798..a3f6fbc31f942 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { kqlQuery } from '../../../../observability/server'; import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/elasticsearch_fieldnames'; import { environmentQuery } from '../../../common/utils/environment_query'; import { getConnectionStats } from '../connections/get_connection_stats'; @@ -17,6 +18,7 @@ export async function getUpstreamServicesForBackend({ end, backendName, numBuckets, + kuery, environment, offset, }: { @@ -25,6 +27,7 @@ export async function getUpstreamServicesForBackend({ end: number; backendName: string; numBuckets: number; + kuery?: string; environment?: string; offset?: string; }) { @@ -35,6 +38,7 @@ export async function getUpstreamServicesForBackend({ filter: [ { term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } }, ...environmentQuery(environment), + ...kqlQuery(kuery), ], collapseBy: 'upstream', numBuckets, diff --git a/x-pack/plugins/apm/server/routes/backends.ts b/x-pack/plugins/apm/server/routes/backends.ts index 93d8bab3f80ff..c738e9aa64007 100644 --- a/x-pack/plugins/apm/server/routes/backends.ts +++ b/x-pack/plugins/apm/server/routes/backends.ts @@ -15,6 +15,8 @@ import { getMetadataForBackend } from '../lib/backends/get_metadata_for_backend' import { getLatencyChartsForBackend } from '../lib/backends/get_latency_charts_for_backend'; import { getTopBackends } from '../lib/backends/get_top_backends'; import { getUpstreamServicesForBackend } from '../lib/backends/get_upstream_services_for_backend'; +import { getThroughputChartsForBackend } from '../lib/backends/get_throughput_charts_for_backend'; +import { getErrorRateChartsForBackend } from '../lib/backends/get_error_rate_charts_for_backend'; const topBackendsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/backends/top_backends', @@ -68,7 +70,7 @@ const upstreamServicesForBackendRoute = createApmServerRoute({ query: t.intersection([rangeRt, t.type({ numBuckets: toNumberRt })]), }), t.partial({ - query: t.intersection([environmentRt, offsetRt]), + query: t.intersection([environmentRt, offsetRt, kueryRt]), }), ]), options: { @@ -80,10 +82,18 @@ const upstreamServicesForBackendRoute = createApmServerRoute({ const { start, end } = setup; const { path: { backendName }, - query: { environment, offset, numBuckets }, + query: { environment, offset, numBuckets, kuery }, } = resources.params; - const opts = { backendName, setup, start, end, numBuckets, environment }; + const opts = { + backendName, + setup, + start, + end, + numBuckets, + environment, + kuery, + }; const [currentServices, previousServices] = await Promise.all([ getUpstreamServicesForBackend(opts), @@ -182,8 +192,100 @@ const backendLatencyChartsRoute = createApmServerRoute({ }, }); +const backendThroughputChartsRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/backends/{backendName}/charts/throughput', + params: t.type({ + path: t.type({ + backendName: t.string, + }), + query: t.intersection([rangeRt, kueryRt, environmentRt, offsetRt]), + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { backendName } = params.path; + const { kuery, environment, offset } = params.query; + + const { start, end } = setup; + + const [currentTimeseries, comparisonTimeseries] = await Promise.all([ + getThroughputChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + }), + offset + ? getThroughputChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + offset, + }) + : null, + ]); + + return { currentTimeseries, comparisonTimeseries }; + }, +}); + +const backendErrorRateChartsRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/backends/{backendName}/charts/error_rate', + params: t.type({ + path: t.type({ + backendName: t.string, + }), + query: t.intersection([rangeRt, kueryRt, environmentRt, offsetRt]), + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { backendName } = params.path; + const { kuery, environment, offset } = params.query; + + const { start, end } = setup; + + const [currentTimeseries, comparisonTimeseries] = await Promise.all([ + getErrorRateChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + }), + offset + ? getErrorRateChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + offset, + }) + : null, + ]); + + return { currentTimeseries, comparisonTimeseries }; + }, +}); + export const backendsRouteRepository = createApmServerRouteRepository() .add(topBackendsRoute) .add(upstreamServicesForBackendRoute) .add(backendMetadataRoute) - .add(backendLatencyChartsRoute); + .add(backendLatencyChartsRoute) + .add(backendThroughputChartsRoute) + .add(backendErrorRateChartsRoute);