Skip to content

Commit

Permalink
[APM] Add error rates to Service Map popovers (#69520)
Browse files Browse the repository at this point in the history
Make the `getErrorRate` function used in the error rate charts additionally take `service.environment` as a filter and have it return the `average` of the values.

Call that function in the API for the service map metrics.

Fixes #68160.

Co-authored-by: cauemarcondes <[email protected]>
  • Loading branch information
smith and cauemarcondes authored Jul 15, 2020
1 parent 3c9fa99 commit f69edbd
Show file tree
Hide file tree
Showing 15 changed files with 568 additions and 351 deletions.
4 changes: 2 additions & 2 deletions x-pack/plugins/apm/common/service_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ export interface Connection {
destination: ConnectionNode;
}

export interface ServiceNodeMetrics {
export interface ServiceNodeStats {
avgMemoryUsage: number | null;
avgCpuUsage: number | null;
transactionStats: {
avgTransactionDuration: number | null;
avgRequestsPerMinute: number | null;
};
avgErrorsPerMinute: number | null;
avgErrorRate: number | null;
}

export function isValidPlatinumLicense(license: ILicense) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import cytoscape from 'cytoscape';
import React, { MouseEvent } from 'react';
import { Buttons } from './Buttons';
import { Info } from './Info';
import { ServiceMetricFetcher } from './ServiceMetricFetcher';
import { ServiceStatsFetcher } from './ServiceStatsFetcher';
import { popoverWidth } from '../cytoscapeOptions';

interface ContentsProps {
Expand Down Expand Up @@ -70,7 +70,7 @@ export function Contents({
</FlexColumnItem>
<FlexColumnItem>
{isService ? (
<ServiceMetricFetcher
<ServiceStatsFetcher
serviceName={selectedNodeServiceName}
serviceAnomalyStats={selectedNodeData.serviceAnomalyStats}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ export function Info(data: InfoProps) {

const listItems = [
{
title: i18n.translate('xpack.apm.serviceMap.typePopoverMetric', {
title: i18n.translate('xpack.apm.serviceMap.typePopoverStat', {
defaultMessage: 'Type',
}),
description: type,
},
{
title: i18n.translate('xpack.apm.serviceMap.subtypePopoverMetric', {
title: i18n.translate('xpack.apm.serviceMap.subtypePopoverStat', {
defaultMessage: 'Subtype',
}),
description: subtype,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,128 @@
*/

import { storiesOf } from '@storybook/react';
import cytoscape from 'cytoscape';
import { HttpSetup } from 'kibana/public';
import React from 'react';
import { ServiceMetricList } from './ServiceMetricList';
import { EuiThemeProvider } from '../../../../../../observability/public';
import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
import { MockUrlParamsContextProvider } from '../../../../context/UrlParamsContext/MockUrlParamsContextProvider';
import { createCallApmApi } from '../../../../services/rest/createCallApmApi';
import { CytoscapeContext } from '../Cytoscape';
import { Popover } from './';
import { ServiceStatsList } from './ServiceStatsList';

storiesOf('app/ServiceMap/Popover/ServiceMetricList', module)
.add('example', () => (
<ServiceMetricList
avgErrorsPerMinute={15.738888706725826}
transactionStats={{
avgTransactionDuration: 61634.38905590272,
storiesOf('app/ServiceMap/Popover', module)
.addDecorator((storyFn) => {
const node = {
data: { id: 'example service', 'service.name': 'example service' },
};
const cy = cytoscape({ elements: [node] });
const httpMock = ({
get: async () => ({
avgCpuUsage: 0.32809666568309237,
avgErrorRate: 0.556068173242986,
avgMemoryUsage: 0.5504868173242986,
avgRequestsPerMinute: 164.47222031860858,
}}
avgCpuUsage={0.32809666568309237}
avgMemoryUsage={0.5504868173242986}
/>
))
.add('some null values', () => (
<ServiceMetricList
avgErrorsPerMinute={7.615972134074397}
transactionStats={{
avgTransactionDuration: 238792.54809512055,
avgRequestsPerMinute: 8.439583235652972,
}}
avgCpuUsage={null}
avgMemoryUsage={null}
/>
))
.add('all null values', () => (
<ServiceMetricList
avgErrorsPerMinute={null}
transactionStats={{
avgTransactionDuration: null,
avgRequestsPerMinute: null,
}}
avgCpuUsage={null}
avgMemoryUsage={null}
/>
));
avgTransactionDuration: 61634.38905590272,
}),
} as unknown) as HttpSetup;

createCallApmApi(httpMock);

setImmediate(() => {
cy.$('example service').select();
});

return (
<EuiThemeProvider>
<MockUrlParamsContextProvider>
<MockApmPluginContextWrapper>
<CytoscapeContext.Provider value={cy}>
<div style={{ height: 325 }}>{storyFn()}</div>
</CytoscapeContext.Provider>
</MockApmPluginContextWrapper>
</MockUrlParamsContextProvider>
</EuiThemeProvider>
);
})
.add(
'example',
() => {
return <Popover />;
},
{
info: {
propTablesExclude: [
CytoscapeContext.Provider,
MockApmPluginContextWrapper,
MockUrlParamsContextProvider,
EuiThemeProvider,
],
source: false,
},
}
);

storiesOf('app/ServiceMap/Popover/ServiceStatsList', module)
.addDecorator((storyFn) => <EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
.add(
'example',
() => (
<ServiceStatsList
avgCpuUsage={0.32809666568309237}
avgMemoryUsage={0.5504868173242986}
transactionStats={{
avgRequestsPerMinute: 164.47222031860858,
avgTransactionDuration: 61634.38905590272,
}}
avgErrorRate={0.556068173242986}
/>
),
{ info: { propTablesExclude: [EuiThemeProvider] } }
)
.add(
'loading',
() => (
<ServiceStatsList
avgCpuUsage={null}
avgErrorRate={null}
avgMemoryUsage={null}
transactionStats={{
avgRequestsPerMinute: null,
avgTransactionDuration: null,
}}
/>
),
{ info: { propTablesExclude: [EuiThemeProvider] } }
)
.add(
'some null values',
() => (
<ServiceStatsList
avgCpuUsage={null}
avgErrorRate={0.615972134074397}
avgMemoryUsage={null}
transactionStats={{
avgRequestsPerMinute: 8.439583235652972,
avgTransactionDuration: 238792.54809512055,
}}
/>
),
{ info: { propTablesExclude: [EuiThemeProvider] } }
)
.add(
'all null values',
() => (
<ServiceStatsList
avgCpuUsage={null}
avgErrorRate={null}
avgMemoryUsage={null}
transactionStats={{
avgRequestsPerMinute: null,
avgTransactionDuration: null,
}}
/>
),
{ info: { propTablesExclude: [EuiThemeProvider] } }
);
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,44 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import { ServiceNodeMetrics } from '../../../../../common/service_map';
import { ServiceNodeStats } from '../../../../../common/service_map';
import { ServiceStatsList } from './ServiceStatsList';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { ServiceMetricList } from './ServiceMetricList';
import { AnomalyDetection } from './AnomalyDetection';
import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection';

interface ServiceMetricFetcherProps {
interface ServiceStatsFetcherProps {
environment?: string;
serviceName: string;
serviceAnomalyStats: ServiceAnomalyStats | undefined;
}

export function ServiceMetricFetcher({
export function ServiceStatsFetcher({
serviceName,
serviceAnomalyStats,
}: ServiceMetricFetcherProps) {
}: ServiceStatsFetcherProps) {
const {
urlParams: { start, end, environment },
urlParams: { start, end },
uiFilters,
} = useUrlParams();

const {
data = { transactionStats: {} } as ServiceNodeMetrics,
data = { transactionStats: {} } as ServiceNodeStats,
status,
} = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/service-map/service/{serviceName}',
params: { path: { serviceName }, query: { start, end, environment } },
params: {
path: { serviceName },
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
},
});
}
},
[serviceName, start, end, environment],
[serviceName, start, end, uiFilters],
{
preservePreviousData: false,
}
Expand All @@ -60,20 +65,20 @@ export function ServiceMetricFetcher({

const {
avgCpuUsage,
avgErrorsPerMinute,
avgErrorRate,
avgMemoryUsage,
transactionStats: { avgRequestsPerMinute, avgTransactionDuration },
} = data;

const hasServiceData = [
avgCpuUsage,
avgErrorsPerMinute,
avgErrorRate,
avgMemoryUsage,
avgRequestsPerMinute,
avgTransactionDuration,
].some((stat) => isNumber(stat));

if (environment && !hasServiceData) {
if (!hasServiceData) {
return (
<EuiText color="subdued">
{i18n.translate('xpack.apm.serviceMap.popoverMetrics.noDataText', {
Expand All @@ -93,7 +98,7 @@ export function ServiceMetricFetcher({
<EuiHorizontalRule margin="xs" />
</>
)}
<ServiceMetricList {...data} />
<ServiceStatsList {...data} />
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { ServiceNodeMetrics } from '../../../../../common/service_map';
import { ServiceNodeStats } from '../../../../../common/service_map';
import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters';

export const ItemRow = styled('tr')`
Expand All @@ -24,18 +24,18 @@ export const ItemDescription = styled('td')`
text-align: right;
`;

type ServiceMetricListProps = ServiceNodeMetrics;
type ServiceStatsListProps = ServiceNodeStats;

export function ServiceMetricList({
avgErrorsPerMinute,
export function ServiceStatsList({
transactionStats,
avgErrorRate,
avgCpuUsage,
avgMemoryUsage,
transactionStats,
}: ServiceMetricListProps) {
}: ServiceStatsListProps) {
const listItems = [
{
title: i18n.translate(
'xpack.apm.serviceMap.avgTransDurationPopoverMetric',
'xpack.apm.serviceMap.avgTransDurationPopoverStat',
{
defaultMessage: 'Trans. duration (avg.)',
}
Expand All @@ -58,27 +58,21 @@ export function ServiceMetricList({
: null,
},
{
title: i18n.translate(
'xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric',
{
defaultMessage: 'Errors per minute (avg.)',
}
),
description: avgErrorsPerMinute?.toFixed(2),
title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', {
defaultMessage: 'Error rate (avg.)',
}),
description: isNumber(avgErrorRate) ? asPercent(avgErrorRate, 1) : null,
},
{
title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverMetric', {
title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', {
defaultMessage: 'CPU usage (avg.)',
}),
description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : null,
},
{
title: i18n.translate(
'xpack.apm.serviceMap.avgMemoryUsagePopoverMetric',
{
defaultMessage: 'Memory usage (avg.)',
}
),
title: i18n.translate('xpack.apm.serviceMap.avgMemoryUsagePopoverStat', {
defaultMessage: 'Memory usage (avg.)',
}),
description: isNumber(avgMemoryUsage)
? asPercent(avgMemoryUsage, 1)
: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Logger } from 'src/core/server';
import { UIFilters } from '../../../../typings/ui_filters';

export function getParsedUiFilters({
uiFilters,
logger,
}: {
uiFilters: string;
logger: Logger;
}): UIFilters {
try {
return JSON.parse(uiFilters);
} catch (error) {
logger.error(error);
}
return {};
}
Loading

0 comments on commit f69edbd

Please sign in to comment.