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 8305b5a0dde3b..8513e0835d373 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 @@ -33,15 +33,16 @@ export interface MainStatsServiceInstanceItem { cpuUsage: number; memoryUsage: number; } +type ApiResponseMainStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics'>; +type ApiResponseDetailedStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; const INITIAL_STATE_MAIN_STATS = { - mainStatsItems: [] as MainStatsServiceInstanceItem[], - mainStatsRequestId: undefined, - mainStatsItemCount: 0, + currentPeriodItems: [] as ApiResponseMainStats['currentPeriod'], + previousPeriodItems: [] as ApiResponseMainStats['previousPeriod'], + requestId: undefined, + currentPeriodItemsCount: 0, }; -type ApiResponseDetailedStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; - const INITIAL_STATE_DETAILED_STATISTICS: ApiResponseDetailedStats = { currentPeriod: {}, previousPeriod: {}, @@ -117,28 +118,17 @@ export function ServiceOverviewInstancesChartAndTable({ start, end, transactionType, + comparisonStart, + comparisonEnd, }, }, }).then((response) => { - const mainStatsItems = orderBy( - // need top-level sortable fields for the managed table - response.serviceInstances.map((item) => ({ - ...item, - latency: item.latency ?? 0, - throughput: item.throughput ?? 0, - errorRate: item.errorRate ?? 0, - cpuUsage: item.cpuUsage ?? 0, - memoryUsage: item.memoryUsage ?? 0, - })), - field, - direction - ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); - return { // Everytime the main statistics is refetched, updates the requestId making the detailed API to be refetched. - mainStatsRequestId: uuid(), - mainStatsItems, - mainStatsItemCount: response.serviceInstances.length, + requestId: uuid(), + currentPeriodItems: response.currentPeriod, + currentPeriodItemsCount: response.currentPeriod.length, + previousPeriodItems: response.previousPeriod, }; }); }, @@ -162,11 +152,26 @@ export function ServiceOverviewInstancesChartAndTable({ ); const { - mainStatsItems, - mainStatsRequestId, - mainStatsItemCount, + currentPeriodItems, + previousPeriodItems, + requestId, + currentPeriodItemsCount, } = mainStatsData; + const currentPeriodOrderedItems = orderBy( + // need top-level sortable fields for the managed table + currentPeriodItems.map((item) => ({ + ...item, + latency: item.latency ?? 0, + throughput: item.throughput ?? 0, + errorRate: item.errorRate ?? 0, + cpuUsage: item.cpuUsage ?? 0, + memoryUsage: item.memoryUsage ?? 0, + })), + field, + direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + const { data: detailedStatsData = INITIAL_STATE_DETAILED_STATISTICS, status: detailedStatsStatus, @@ -177,7 +182,7 @@ export function ServiceOverviewInstancesChartAndTable({ !end || !transactionType || !latencyAggregationType || - !mainStatsItemCount + !currentPeriodItemsCount ) { return; } @@ -198,7 +203,7 @@ export function ServiceOverviewInstancesChartAndTable({ numBuckets: 20, transactionType, serviceNodeIds: JSON.stringify( - mainStatsItems.map((item) => item.serviceNodeName) + currentPeriodOrderedItems.map((item) => item.serviceNodeName) ), comparisonStart, comparisonEnd, @@ -208,7 +213,7 @@ export function ServiceOverviewInstancesChartAndTable({ }, // only fetches detailed statistics when requestId is invalidated by main statistics api call // eslint-disable-next-line react-hooks/exhaustive-deps - [mainStatsRequestId], + [requestId], { preservePreviousData: false } ); @@ -217,16 +222,17 @@ export function ServiceOverviewInstancesChartAndTable({

{i18n.translate('xpack.apm.serviceOverview.instancesTableTitle', { - defaultMessage: 'All instances', + defaultMessage: 'Instances', })}

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 394d5b5410d41..ce4f36ced7903 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 @@ -30,33 +30,32 @@ import { } from '../../../../../common/utils/formatters'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; -import { MainStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import * as urlHelpers from '../../Links/url_helpers'; import { ChartContainer } from '../chart_container'; import { getResponseTimeTickFormatter } from '../transaction_charts/helper'; import { CustomTooltip } from './custom_tooltip'; +type ApiResponseMainStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics'>; + export interface InstancesLatencyDistributionChartProps { height: number; - items?: MainStatsServiceInstanceItem[]; + items?: ApiResponseMainStats['currentPeriod']; status: FETCH_STATUS; + comparisonItems?: ApiResponseMainStats['previousPeriod']; } export function InstancesLatencyDistributionChart({ height, items = [], status, + comparisonItems = [], }: InstancesLatencyDistributionChartProps) { const history = useHistory(); const hasData = items.length > 0; const theme = useTheme(); - const chartTheme = { - ...useChartTheme(), - bubbleSeriesStyle: { - point: { strokeWidth: 0, fill: theme.eui.euiColorVis1, radius: 4 }, - }, - }; + const chartTheme = useChartTheme(); const maxLatency = Math.max(...items.map((item) => item.latency ?? 0)); const latencyFormatter = getDurationFormatter(maxLatency); @@ -96,7 +95,13 @@ export function InstancesLatencyDistributionChart({ // 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 }; + const maxComparisonThroughput = Math.max( + ...comparisonItems.map((item) => item.throughput ?? 0) + ); + const xDomain = { + min: 0, + max: Math.max(maxThroughput, maxComparisonThroughput), + }; return ( @@ -118,7 +123,7 @@ export function InstancesLatencyDistributionChart({ xDomain={xDomain} /> item.latency]} yScaleType={ScaleType.Linear} + bubbleSeriesStyle={{ + point: { + strokeWidth: 0, + radius: 4, + fill: theme.eui.euiColorVis0, + }, + }} /> + + {!!comparisonItems.length && ( + item.throughput} + xScaleType={ScaleType.Linear} + yAccessors={[(item) => item.latency]} + yScaleType={ScaleType.Linear} + color={theme.eui.euiColorMediumShade} + bubbleSeriesStyle={{ + point: { + shape: 'square', + radius: 4, + fill: theme.eui.euiColorLightestShade, + stroke: theme.eui.euiColorMediumShade, + strokeWidth: 2, + }, + }} + /> + )} { describe('fetching java data', () => { @@ -72,11 +75,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns a service node item', () => { - expect(response.body.serviceInstances.length).to.be.greaterThan(0); + expect(response.body.currentPeriod.length).to.be.greaterThan(0); }); it('returns statistics for each service node', () => { - const item = response.body.serviceInstances[0]; + const item = response.body.currentPeriod[0]; expect(isFiniteNumber(item.cpuUsage)).to.be(true); expect(isFiniteNumber(item.memoryUsage)).to.be(true); @@ -86,7 +89,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right data', () => { - const items = sortBy(response.body.serviceInstances, 'serviceNodeName'); + const items = sortBy(response.body.currentPeriod, 'serviceNodeName'); const serviceNodeNames = items.map((item) => item.serviceNodeName); @@ -141,7 +144,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns statistics for each service node', () => { - const item = response.body.serviceInstances[0]; + const item = response.body.currentPeriod[0]; expect(isFiniteNumber(item.cpuUsage)).to.be(true); expect(isFiniteNumber(item.memoryUsage)).to.be(true); @@ -151,7 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right data', () => { - const items = sortBy(response.body.serviceInstances, 'serviceNodeName'); + const items = sortBy(response.body.currentPeriod, 'serviceNodeName'); const serviceNodeNames = items.map((item) => item.serviceNodeName); @@ -181,4 +184,90 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } ); + + registry.when( + 'Service overview instances main statistics when data is loaded with comparison', + { config: 'basic', archives: [archiveName] }, + () => { + describe('fetching java data', () => { + let response: { + body: APIReturnType<`GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`>; + }; + + beforeEach(async () => { + response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`, + params: { + path: { serviceName: 'opbeans-java' }, + query: { + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + }, + }, + }); + }); + + it('returns a service node item', () => { + expect(response.body.currentPeriod.length).to.be.greaterThan(0); + expect(response.body.previousPeriod.length).to.be.greaterThan(0); + }); + + it('returns statistics for each service node', () => { + const currentItem = response.body.currentPeriod[0]; + + expect(isFiniteNumber(currentItem.cpuUsage)).to.be(true); + expect(isFiniteNumber(currentItem.memoryUsage)).to.be(true); + expect(isFiniteNumber(currentItem.errorRate)).to.be(true); + expect(isFiniteNumber(currentItem.throughput)).to.be(true); + expect(isFiniteNumber(currentItem.latency)).to.be(true); + + const previousItem = response.body.previousPeriod[0]; + + expect(isFiniteNumber(previousItem.cpuUsage)).to.be(true); + expect(isFiniteNumber(previousItem.memoryUsage)).to.be(true); + expect(isFiniteNumber(previousItem.errorRate)).to.be(true); + expect(isFiniteNumber(previousItem.throughput)).to.be(true); + expect(isFiniteNumber(previousItem.latency)).to.be(true); + }); + + it('returns the right data', () => { + const items = sortBy(response.body.previousPeriod, 'serviceNodeName'); + + const serviceNodeNames = items.map((item) => item.serviceNodeName); + + expectSnapshot(items.length).toMatchInline(`1`); + + expectSnapshot(serviceNodeNames).toMatchInline(` + Array [ + "02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c", + ] + `); + + const item = items[0]; + + const values = pick(item, [ + 'cpuUsage', + 'memoryUsage', + 'errorRate', + 'throughput', + 'latency', + ]); + + expectSnapshot(values).toMatchInline(` + Object { + "cpuUsage": 0.0120666666666667, + "errorRate": 0.111111111111111, + "latency": 379742.555555556, + "memoryUsage": 0.939879608154297, + "throughput": 3, + } + `); + }); + }); + } + ); }