Instances latency distribution chart tooltips and axis fixes (#95577)

Fixes #88852
This commit is contained in:
Nathan L Smith 2021-04-13 16:02:55 -05:00 committed by GitHub
parent 355c949463
commit dfca5d440c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 586 additions and 39 deletions

View file

@ -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)',
}
);

View file

@ -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;
}

View file

@ -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,

View file

@ -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',
{

View file

@ -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 (
<>
{/* <EuiFlexItem grow={3}>
<EuiFlexItem grow={3}>
<InstancesLatencyDistributionChart
height={chartHeight}
items={data.items}
status={status}
items={primaryStatsItems}
status={primaryStatsStatus}
/>
</EuiFlexItem> */}
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiPanel>
<ServiceOverviewInstancesTable

View file

@ -10,8 +10,10 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import { isJavaAgentName } from '../../../../../common/agent_name';
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 {
asMillisecondDuration,
asPercent,
@ -52,9 +54,7 @@ export function getColumns({
const { serviceNodeName } = item;
const isMissingServiceNodeName =
serviceNodeName === SERVICE_NODE_NAME_MISSING;
const text = isMissingServiceNodeName
? UNIDENTIFIED_SERVICE_NODES_LABEL
: serviceNodeName;
const text = getServiceNodeName(serviceNodeName);
const link = isJavaAgentName(agentName) ? (
<ServiceNodeMetricOverviewLink

View file

@ -0,0 +1,181 @@
/*
* 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 React, { ComponentType } from 'react';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
import { PrimaryStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table';
import { CustomTooltip } from './custom_tooltip';
function getLatencyFormatter(props: TooltipInfo) {
const maxLatency = Math.max(
...props.values.map((value) => {
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) => (
<EuiThemeProvider>
<Story />
</EuiThemeProvider>
),
],
};
export function Example(props: TooltipInfo) {
return (
<CustomTooltip {...props} latencyFormatter={getLatencyFormatter(props)} />
);
}
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 (
<CustomTooltip {...props} latencyFormatter={getLatencyFormatter(props)} />
);
}
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;

View file

@ -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 (
<>
<div className="echTooltip__header">
{getServiceNodeName(serviceNodeName)}
</div>
<div className="echTooltip__list">
<div className="echTooltip__item">
<div
className="echTooltip__item--backgroundColor"
style={{ backgroundColor: 'transparent' }}
>
<div
className="echTooltip__item--color"
style={{ backgroundColor: color }}
/>
</div>
<div className="echTooltip__item--container">
<span className="echTooltip__label">{latencyLabel}</span>
<span className="echTooltip__value">
{latencyFormatter(latency).formatted}
</span>
</div>
</div>
<div className="echTooltip__item">
<div
className="echTooltip__item--backgroundColor"
style={{ backgroundColor: 'transparent' }}
>
<div
className="echTooltip__item--color"
style={{ backgroundColor: color }}
/>
</div>
<div className="echTooltip__item--container">
<span className="echTooltip__label">{throughputLabel}</span>
<span className="echTooltip__value">
{asTransactionRate(throughput)}
</span>
</div>
</div>
</div>
</>
);
}
/**
* Tooltip for a multiple instances
*/
function MultipleInstanceCustomTooltip({
latencyFormatter,
values,
}: TooltipInfo & { latencyFormatter: TimeFormatter }) {
const theme = useTheme();
return (
<>
<div className="echTooltip__header">
{i18n.translate(
'xpack.apm.instancesLatencyDistributionChartTooltipInstancesTitle',
{
defaultMessage:
'{instancesCount} {instancesCount, plural, one {instance} other {instances}}',
values: { instancesCount: values.length },
}
)}
</div>
{values.map((value) => {
const { color } = value;
const datum = (value.datum as unknown) as PrimaryStatsServiceInstanceItem;
const { latency, serviceNodeName, throughput } = datum;
return (
<div className="echTooltip__list" key={serviceNodeName}>
<div className="echTooltip__item">
<div
className="echTooltip__item--backgroundColor"
style={{ backgroundColor: 'transparent' }}
>
<div
className="echTooltip__item--color"
style={{ backgroundColor: color }}
/>
</div>
<div className="echTooltip__item--container">
<span className="echTooltip__label">
{getServiceNodeName(serviceNodeName)}
</span>
</div>
</div>
<div className="echTooltip__item">
<div
className="echTooltip__item--backgroundColor"
style={{ backgroundColor: 'transparent' }}
>
<div
className="echTooltip__item--color"
style={{ backgroundColor: color }}
/>
</div>
<div
className="echTooltip__item--container"
style={{ paddingLeft: theme.eui.paddingSizes.s }}
>
<span className="echTooltip__label">{latencyLabel}</span>
<span className="echTooltip__value">
{latencyFormatter(latency).formatted}
</span>
</div>
</div>
<div className="echTooltip__item">
<div
className="echTooltip__item--backgroundColor"
style={{ backgroundColor: 'transparent' }}
>
<div
className="echTooltip__item--color"
style={{ backgroundColor: color }}
/>
</div>
<div
className="echTooltip__item--container"
style={{ paddingLeft: theme.eui.paddingSizes.s }}
>
<span className="echTooltip__label">{throughputLabel}</span>
<span className="echTooltip__value">
{asTransactionRate(throughput)}
</span>
</div>
</div>
</div>
);
})}
</>
);
}
/**
* 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 (
<div className="echTooltip">
{values.length > 1 ? (
<MultipleInstanceCustomTooltip {...props} />
) : (
<SingleInstanceCustomTooltip {...props} />
)}
<div style={{ padding: theme.eui.paddingSizes.xs }}>
<EuiIcon type="filter" /> {clickToFilterDescription}
</div>
</div>
);
}

View file

@ -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) => (
<CustomTooltip {...props} latencyFormatter={latencyFormatter} />
),
};
/**
* 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 (
<EuiPanel>
<EuiTitle size="xs">
@ -64,9 +111,11 @@ export function InstancesLatencyDistributionChart({
<Chart id="instances-latency-distribution">
<Settings
legendPosition={Position.Bottom}
tooltip="none"
onElementClick={handleElementClick}
tooltip={tooltip}
showLegend
theme={chartTheme}
xDomain={xDomain}
/>
<BubbleSeries
color={theme.eui.euiColorVis1}

View file

@ -0,0 +1,108 @@
/*
* 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, { ComponentType } from 'react';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import {
InstancesLatencyDistributionChart,
InstancesLatencyDistributionChartProps,
} from './';
export default {
title: 'shared/charts/InstancesLatencyDistributionChart',
component: InstancesLatencyDistributionChart,
decorators: [
(Story: ComponentType) => (
<EuiThemeProvider>
<Story />
</EuiThemeProvider>
),
],
};
export function Example({ items }: InstancesLatencyDistributionChartProps) {
return (
<InstancesLatencyDistributionChart
height={300}
items={items}
status={FETCH_STATUS.SUCCESS}
/>
);
}
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 (
<InstancesLatencyDistributionChart
height={300}
items={items}
status={FETCH_STATUS.SUCCESS}
/>
);
}
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;