From dfca5d440c5cf5f2fb900d5427a2ca03b812331d Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 13 Apr 2021 16:02:55 -0500 Subject: [PATCH] Instances latency distribution chart tooltips and axis fixes (#95577) Fixes #88852 --- x-pack/plugins/apm/common/i18n.ts | 7 - x-pack/plugins/apm/common/service_nodes.ts | 15 ++ .../app/Main/route_config/index.tsx | 13 +- .../app/service_node_overview/index.tsx | 8 +- ...ice_overview_instances_chart_and_table.tsx | 16 +- .../get_columns.tsx | 10 +- .../custom_tooltip.stories.tsx | 181 +++++++++++++++ .../custom_tooltip.tsx | 214 ++++++++++++++++++ .../index.tsx | 53 ++++- ...ces_latency_distribution_chart.stories.tsx | 108 +++++++++ 10 files changed, 586 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx diff --git a/x-pack/plugins/apm/common/i18n.ts b/x-pack/plugins/apm/common/i18n.ts index c5bbef0db244e..8bce2acdf4dca 100644 --- a/x-pack/plugins/apm/common/i18n.ts +++ b/x-pack/plugins/apm/common/i18n.ts @@ -13,10 +13,3 @@ export const NOT_AVAILABLE_LABEL = i18n.translate( defaultMessage: 'N/A', } ); - -export const UNIDENTIFIED_SERVICE_NODES_LABEL = i18n.translate( - 'xpack.apm.serviceNodeNameMissing', - { - defaultMessage: '(Empty)', - } -); diff --git a/x-pack/plugins/apm/common/service_nodes.ts b/x-pack/plugins/apm/common/service_nodes.ts index d744330f17b66..ad75bd025069d 100644 --- a/x-pack/plugins/apm/common/service_nodes.ts +++ b/x-pack/plugins/apm/common/service_nodes.ts @@ -5,4 +5,19 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; + export const SERVICE_NODE_NAME_MISSING = '_service_node_name_missing_'; + +const UNIDENTIFIED_SERVICE_NODES_LABEL = i18n.translate( + 'xpack.apm.serviceNodeNameMissing', + { + defaultMessage: '(Empty)', + } +); + +export function getServiceNodeName(serviceNodeName?: string) { + return serviceNodeName === SERVICE_NODE_NAME_MISSING || !serviceNodeName + ? UNIDENTIFIED_SERVICE_NODES_LABEL + : serviceNodeName; +} diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index a7cbd7a79b4a7..0ed9c5c919ddb 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -9,8 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context'; -import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; +import { getServiceNodeName } from '../../../../../common/service_nodes'; import { APMRouteDefinition } from '../../../../application/routes'; import { toQuery } from '../../../shared/Links/url_helpers'; import { ErrorGroupDetails } from '../../ErrorGroupDetails'; @@ -294,15 +293,7 @@ export const routes: APMRouteDefinition[] = [ exact: true, path: '/services/:serviceName/nodes/:serviceNodeName/metrics', component: withApmServiceContext(ServiceNodeMetrics), - breadcrumb: ({ match }) => { - const { serviceNodeName } = match.params; - - if (serviceNodeName === SERVICE_NODE_NAME_MISSING) { - return UNIDENTIFIED_SERVICE_NODES_LABEL; - } - - return serviceNodeName || ''; - }, + breadcrumb: ({ match }) => getServiceNodeName(match.params.serviceNodeName), }, { exact: true, diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index fc218f3ba6df3..3d284de621ea3 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -8,8 +8,10 @@ import { EuiFlexGroup, EuiPage, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { + getServiceNodeName, + SERVICE_NODE_NAME_MISSING, +} from '../../../../common/service_nodes'; import { asDynamicBytes, asInteger, @@ -83,7 +85,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { const { displayedName, tooltip } = name === SERVICE_NODE_NAME_MISSING ? { - displayedName: UNIDENTIFIED_SERVICE_NODES_LABEL, + displayedName: getServiceNodeName(name), tooltip: i18n.translate( 'xpack.apm.jvmsTable.explainServiceNodeNameMissing', { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index 13322b094c65e..55eb2e3ddab73 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -13,19 +13,13 @@ import { useApmServiceContext } from '../../../context/apm_service/use_apm_servi import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { InstancesLatencyDistributionChart } from '../../shared/charts/instances_latency_distribution_chart'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; import { ServiceOverviewInstancesTable, TableOptions, } from './service_overview_instances_table'; -// We're hiding this chart until these issues are resolved in the 7.13 timeframe: -// -// * [[APM] Tooltips for instances latency distribution chart](https://github.com/elastic/kibana/issues/88852) -// * [[APM] x-axis on the instance bubble chart is broken](https://github.com/elastic/kibana/issues/92631) -// -// import { InstancesLatencyDistributionChart } from '../../shared/charts/instances_latency_distribution_chart'; - interface ServiceOverviewInstancesChartAndTableProps { chartHeight: number; serviceName: string; @@ -215,13 +209,13 @@ export function ServiceOverviewInstancesChartAndTable({ return ( <> - {/* + - */} + { + const datum = (value.datum as unknown) as PrimaryStatsServiceInstanceItem; + return datum.latency ?? 0; + }) + ); + return getDurationFormatter(maxLatency); +} + +export default { + title: 'shared/charts/InstancesLatencyDistributionChart/CustomTooltip', + component: CustomTooltip, + decorators: [ + (Story: ComponentType) => ( + + + + ), + ], +}; + +export function Example(props: TooltipInfo) { + return ( + + ); +} +Example.args = { + header: { + seriesIdentifier: { + key: + 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}', + specId: 'Instances', + yAccessor: '(index:0)', + splitAccessors: {}, + seriesKeys: ['(index:0)'], + }, + valueAccessor: 'y1', + label: 'Instances', + value: 9.473837632998105, + formattedValue: '9.473837632998105', + markValue: null, + color: '#6092c0', + isHighlighted: false, + isVisible: true, + datum: { + serviceNodeName: + '2f3221afa3f00d3bc07069d69efd5bd4c1607be6155a204551c8fe2e2b5dd750', + errorRate: 0.03496503496503497, + latency: 1057231.4125874126, + throughput: 9.473837632998105, + cpuUsage: 0.000033333333333333335, + memoryUsage: 0.18701022939403547, + }, + }, + values: [ + { + seriesIdentifier: { + key: + 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}', + specId: 'Instances', + }, + valueAccessor: 'y1', + label: 'Instances', + value: 1057231.4125874126, + formattedValue: '1057231.4125874126', + markValue: null, + color: '#6092c0', + isHighlighted: true, + isVisible: true, + datum: { + serviceNodeName: + '2f3221afa3f00d3bc07069d69efd5bd4c1607be6155a204551c8fe2e2b5dd750', + errorRate: 0.03496503496503497, + latency: 1057231.4125874126, + throughput: 9.473837632998105, + cpuUsage: 0.000033333333333333335, + memoryUsage: 0.18701022939403547, + }, + }, + ], +} as TooltipInfo; + +export function MultipleInstances(props: TooltipInfo) { + return ( + + ); +} +MultipleInstances.args = { + header: { + seriesIdentifier: { + key: + 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}', + specId: 'Instances', + yAccessor: '(index:0)', + splitAccessors: {}, + seriesKeys: ['(index:0)'], + }, + valueAccessor: 'y1', + label: 'Instances', + value: 9.606338858634443, + formattedValue: '9.606338858634443', + markValue: null, + color: '#6092c0', + isHighlighted: false, + isVisible: true, + datum: { + serviceNodeName: + '3b50ad269c45be69088905c4b355cc75ab94aaac1b35432bb752050438f4216f', + errorRate: 0.006896551724137931, + latency: 56465.53793103448, + throughput: 9.606338858634443, + cpuUsage: 0.0001, + memoryUsage: 0.1872131360014741, + }, + }, + values: [ + { + seriesIdentifier: { + key: + 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}', + specId: 'Instances', + }, + valueAccessor: 'y1', + label: 'Instances', + value: 56465.53793103448, + formattedValue: '56465.53793103448', + markValue: null, + color: '#6092c0', + isHighlighted: true, + isVisible: true, + datum: { + serviceNodeName: + '3b50ad269c45be69088905c4b355cc75ab94aaac1b35432bb752050438f4216f', + errorRate: 0.006896551724137931, + latency: 56465.53793103448, + throughput: 9.606338858634443, + cpuUsage: 0.0001, + memoryUsage: 0.1872131360014741, + }, + }, + { + seriesIdentifier: { + key: + 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}', + specId: 'Instances', + }, + valueAccessor: 'y1', + label: 'Instances', + value: 56465.53793103448, + formattedValue: '56465.53793103448', + markValue: null, + color: '#6092c0', + isHighlighted: true, + isVisible: true, + datum: { + serviceNodeName: + '3b50ad269c45be69088905c4b355cc75ab94aaac1b35432bb752050438f4216f (2)', + errorRate: 0.006896551724137931, + latency: 56465.53793103448, + throughput: 9.606338858634443, + cpuUsage: 0.0001, + memoryUsage: 0.1872131360014741, + }, + }, + ], +} as TooltipInfo; diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.tsx new file mode 100644 index 0000000000000..2280fa91a659c --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.tsx @@ -0,0 +1,214 @@ +/* + * 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 { TooltipInfo } from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { getServiceNodeName } from '../../../../../common/service_nodes'; +import { + asTransactionRate, + TimeFormatter, +} from '../../../../../common/utils/formatters'; +import { useTheme } from '../../../../hooks/use_theme'; +import { PrimaryStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table'; + +const latencyLabel = i18n.translate( + 'xpack.apm.instancesLatencyDistributionChartTooltipLatencyLabel', + { + defaultMessage: 'Latency', + } +); + +const throughputLabel = i18n.translate( + 'xpack.apm.instancesLatencyDistributionChartTooltipThroughputLabel', + { + defaultMessage: 'Throughput', + } +); + +const clickToFilterDescription = i18n.translate( + 'xpack.apm.instancesLatencyDistributionChartTooltipClickToFilterDescription', + { defaultMessage: 'Click to filter by instance' } +); + +/** + * Tooltip for a single instance + */ +function SingleInstanceCustomTooltip({ + latencyFormatter, + values, +}: { + latencyFormatter: TimeFormatter; + values: TooltipInfo['values']; +}) { + const value = values[0]; + const { color } = value; + const datum = (value.datum as unknown) as PrimaryStatsServiceInstanceItem; + const { latency, serviceNodeName, throughput } = datum; + + return ( + <> +
+ {getServiceNodeName(serviceNodeName)} +
+
+
+
+
+
+
+ {latencyLabel} + + {latencyFormatter(latency).formatted} + +
+
+
+
+
+
+
+ {throughputLabel} + + {asTransactionRate(throughput)} + +
+
+
+ + ); +} + +/** + * Tooltip for a multiple instances + */ +function MultipleInstanceCustomTooltip({ + latencyFormatter, + values, +}: TooltipInfo & { latencyFormatter: TimeFormatter }) { + const theme = useTheme(); + + return ( + <> +
+ {i18n.translate( + 'xpack.apm.instancesLatencyDistributionChartTooltipInstancesTitle', + { + defaultMessage: + '{instancesCount} {instancesCount, plural, one {instance} other {instances}}', + values: { instancesCount: values.length }, + } + )} +
+ {values.map((value) => { + const { color } = value; + const datum = (value.datum as unknown) as PrimaryStatsServiceInstanceItem; + const { latency, serviceNodeName, throughput } = datum; + return ( +
+
+
+
+
+
+ + {getServiceNodeName(serviceNodeName)} + +
+
+
+
+
+
+
+ {latencyLabel} + + {latencyFormatter(latency).formatted} + +
+
+
+
+
+
+
+ {throughputLabel} + + {asTransactionRate(throughput)} + +
+
+
+ ); + })} + + ); +} + +/** + * Custom tooltip for instances latency distribution chart. + * + * The styling provided here recreates that in the Elastic Charts tooltip: https://github.com/elastic/elastic-charts/blob/58e6b5fbf77f4471d2a9a41c45a61f79ebd89b65/src/components/tooltip/tooltip.tsx + * + * We probably won't need to do all of this once https://github.com/elastic/elastic-charts/issues/615 is completed. + */ +export function CustomTooltip( + props: TooltipInfo & { latencyFormatter: TimeFormatter } +) { + const { values } = props; + const theme = useTheme(); + + return ( +
+ {values.length > 1 ? ( + + ) : ( + + )} +
+ {clickToFilterDescription} +
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx index 5bcf0d161653e..57ecbd4ca0b78 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx @@ -9,14 +9,21 @@ import { Axis, BubbleSeries, Chart, + ElementClickListener, + GeometryValue, Position, ScaleType, Settings, + TooltipInfo, + TooltipProps, + TooltipType, } from '@elastic/charts'; import { EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../../observability/public'; +import { SERVICE_NODE_NAME } from '../../../../../common/elasticsearch_fieldnames'; import { asTransactionRate, getDurationFormatter, @@ -24,10 +31,12 @@ import { import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; import { PrimaryStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table'; +import * as urlHelpers from '../../Links/url_helpers'; import { ChartContainer } from '../chart_container'; import { getResponseTimeTickFormatter } from '../transaction_charts/helper'; +import { CustomTooltip } from './custom_tooltip'; -interface InstancesLatencyDistributionChartProps { +export interface InstancesLatencyDistributionChartProps { height: number; items?: PrimaryStatsServiceInstanceItem[]; status: FETCH_STATUS; @@ -38,6 +47,7 @@ export function InstancesLatencyDistributionChart({ items = [], status, }: InstancesLatencyDistributionChartProps) { + const history = useHistory(); const hasData = items.length > 0; const theme = useTheme(); @@ -51,6 +61,43 @@ export function InstancesLatencyDistributionChart({ const maxLatency = Math.max(...items.map((item) => item.latency ?? 0)); const latencyFormatter = getDurationFormatter(maxLatency); + const tooltip: TooltipProps = { + type: TooltipType.Follow, + snap: false, + customTooltip: (props: TooltipInfo) => ( + + ), + }; + + /** + * Handle click events on the items. + * + * Due to how we handle filtering by using the kuery bar, it's difficult to + * modify existing queries. If you have an existing query in the bar, this will + * wipe it out. This is ok for now, since we probably will be replacing this + * interaction with something nicer in a future release. + * + * The event object has an array two items for each point, one of which has + * the serviceNodeName, so we flatten the list and get the items we need to + * form a query. + */ + const handleElementClick: ElementClickListener = (event) => { + const serviceNodeNamesQuery = event + .flat() + .flatMap((value) => (value as GeometryValue).datum?.serviceNodeName) + .filter((serviceNodeName) => !!serviceNodeName) + .map((serviceNodeName) => `${SERVICE_NODE_NAME}:"${serviceNodeName}"`) + .join(' OR '); + + urlHelpers.push(history, { query: { kuery: serviceNodeNamesQuery } }); + }; + + // With a linear scale, if all the instances have similar throughput (or if + // there's just a single instance) they'll show along the origin. Make sure + // the x-axis domain is [0, maxThroughput]. + const maxThroughput = Math.max(...items.map((item) => item.throughput ?? 0)); + const xDomain = { min: 0, max: maxThroughput }; + return ( @@ -64,9 +111,11 @@ export function InstancesLatencyDistributionChart({ ( + + + + ), + ], +}; + +export function Example({ items }: InstancesLatencyDistributionChartProps) { + return ( + + ); +} +Example.args = { + items: [ + { + serviceNodeName: + '3f67bfc39c7891dc0c5657befb17bf58c19cf10f99472cf8df263c8e5bb1c766', + latency: 15802930.92133213, + throughput: 0.4019360641691481, + }, + { + serviceNodeName: + 'd52c64bea9327f3e960ac1cb63c1b7ea922e3cb3d76ab9b254e57a7cb2f760a0', + latency: 8296442.578550679, + throughput: 0.3932978392703585, + }, + { + serviceNodeName: + '797e0a906ad342223468ca51b663e1af8bdeb40bab376c46c7f7fa2021349290', + latency: 34842576.51204916, + throughput: 0.3353931699532713, + }, + { + serviceNodeName: + '21e1c648bd73434a8a1bf6e849817930e8b43eacf73a5c39c30520ee3b79d8c0', + latency: 40713854.354498595, + throughput: 0.32947224189485164, + }, + { + serviceNodeName: + 'a1c99c8675372af4c74bb01cc48e75989faa6f010a4ccb027df1c410dde0c72c', + latency: 18565471.348388012, + throughput: 0.3261219384041683, + }, + { + serviceNodeName: '_service_node_name_missing_', + latency: 20065471.348388012, + throughput: 0.3261219384041683, + }, + ], +} as InstancesLatencyDistributionChartProps; + +export function SimilarThroughputInstances({ + items, +}: InstancesLatencyDistributionChartProps) { + return ( + + ); +} +SimilarThroughputInstances.args = { + items: [ + { + serviceNodeName: + '21e1c648bd73434a8a1bf6e849817930e8b43eacf73a5c39c30520ee3b79d8c0', + latency: 40713854.354498595, + throughput: 0.3261219384041683, + }, + { + serviceNodeName: + 'a1c99c8675372af4c74bb01cc48e75989faa6f010a4ccb027df1c410dde0c72c', + latency: 18565471.348388012, + throughput: 0.3261219384041683, + }, + { + serviceNodeName: '_service_node_name_missing_', + latency: 20065471.348388012, + throughput: 0.3261219384041683, + }, + ], +} as InstancesLatencyDistributionChartProps;