Skip to content

Commit

Permalink
[APM] Add error rate chart to Errors overview and detail views (#67327)…
Browse files Browse the repository at this point in the history
… (#69490)

* creating error rate chart

* adding error line chart

* creating error rate chart

* using date_histogram

* reapplying prettier style

* changing to theme color

* dont sync tooltips

* adding avg on error charts

* addressing pr comments

* adding possibility to disable legend toggle

* removing x-axis ticks from histogram

* return no percent when transaction count doesn return hits

* addressing PR comments

* addressing PR comments

* returning null when there is no transaction count

Co-authored-by: Elastic Machine <[email protected]>

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
cauemarcondes and elasticmachine authored Jun 23, 2020
1 parent 00e12fd commit e65163b
Show file tree
Hide file tree
Showing 11 changed files with 534 additions and 223 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
*/

import { EuiTitle } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { scaleUtc } from 'd3-scale';
import d3 from 'd3';
import { scaleUtc } from 'd3-scale';
import mean from 'lodash.mean';
import React from 'react';
import { asRelativeDateTimeRange } from '../../../../utils/formatters';
import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs';
Expand All @@ -17,7 +20,7 @@ import { EmptyMessage } from '../../../shared/EmptyMessage';

interface IBucket {
key: number;
count: number;
count: number | undefined;
}

// TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse)
Expand All @@ -30,7 +33,7 @@ interface IDistribution {
interface FormattedBucket {
x0: number;
x: number;
y: number;
y: number | undefined;
}

export function getFormattedBuckets(
Expand Down Expand Up @@ -64,7 +67,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
distribution.bucketSize
);

if (!buckets || distribution.noHits) {
if (!buckets) {
return (
<EmptyMessage
heading={i18n.translate('xpack.apm.errorGroupDetails.noErrorsLabel', {
Expand All @@ -74,6 +77,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
);
}

const averageValue = mean(buckets.map((bucket) => bucket.y)) || 0;
const xMin = d3.min(buckets, (d) => d.x0);
const xMax = d3.max(buckets, (d) => d.x);
const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat();
Expand All @@ -84,6 +88,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
<span>{title}</span>
</EuiTitle>
<Histogram
noHits={distribution.noHits}
tooltipHeader={tooltipHeader}
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
xType="time-utc"
Expand All @@ -105,6 +110,17 @@ export function ErrorDistribution({ distribution, title }: Props) {
values: { occCount: value },
})
}
legends={[
{
color: theme.euiColorVis1,
// 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m
legendValue: numeral(averageValue).format('0a'),
title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', {
defaultMessage: 'Avg.',
}),
legendClickDisabled: true,
},
]}
/>
</div>
);
Expand Down
107 changes: 56 additions & 51 deletions x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import { ErrorDistribution } from './Distribution';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTrackPageview } from '../../../../../observability/public';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { ErrorRateChart } from '../../shared/charts/ErrorRateChart';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';

const Titles = styled.div`
margin-bottom: ${px(units.plus)};
Expand Down Expand Up @@ -60,49 +63,43 @@ export function ErrorGroupDetails() {
const { urlParams, uiFilters } = useUrlParams();
const { serviceName, start, end, errorGroupId } = urlParams;

const { data: errorGroupData } = useFetcher(
(callApmApi) => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
groupId: errorGroupId,
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
},
const { data: errorGroupData } = useFetcher(() => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
groupId: errorGroupId,
},
});
}
},
[serviceName, start, end, errorGroupId, uiFilters]
);

const { data: errorDistributionData } = useFetcher(
(callApmApi) => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName,
},
query: {
start,
end,
groupId: errorGroupId,
uiFilters: JSON.stringify(uiFilters),
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
},
});
}
},
[serviceName, start, end, errorGroupId, uiFilters]
);
},
});
}
}, [serviceName, start, end, errorGroupId, uiFilters]);

const { data: errorDistributionData } = useFetcher(() => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName,
},
query: {
start,
end,
groupId: errorGroupId,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, errorGroupId, uiFilters]);

useTrackPageview({ app: 'apm', path: 'error_group_details' });
useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 });
Expand Down Expand Up @@ -184,16 +181,24 @@ export function ErrorGroupDetails() {
</EuiText>
</Titles>
)}

<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
{
defaultMessage: 'Occurrences',
}
)}
/>
<EuiFlexGroup gutterSize="s">
<ChartsSyncContextProvider>
<EuiFlexItem>
<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.errorGroupDetails.occurrencesChartLabel',
{
defaultMessage: 'Occurrences',
}
)}
/>
</EuiFlexItem>
<EuiFlexItem>
<ErrorRateChart />
</EuiFlexItem>
</ChartsSyncContextProvider>
</EuiFlexGroup>
</EuiPanel>
<EuiSpacer size="s" />
{showDetails && (
Expand Down
122 changes: 63 additions & 59 deletions x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,61 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { useFetcher } from '../../../hooks/useFetcher';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
import { ErrorGroupList } from './List';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTrackPageview } from '../../../../../observability/public';
import { PROJECTION } from '../../../../common/projections/typings';
import { useFetcher } from '../../../hooks/useFetcher';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { ErrorRateChart } from '../../shared/charts/ErrorRateChart';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
import { ErrorGroupList } from './List';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';

const ErrorGroupOverview: React.FC = () => {
const { urlParams, uiFilters } = useUrlParams();

const { serviceName, start, end, sortField, sortDirection } = urlParams;

const { data: errorDistributionData } = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName,
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
},
const { data: errorDistributionData } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName,
},
});
}
},
[serviceName, start, end, uiFilters]
);
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, uiFilters]);

const { data: errorGroupListData } = useFetcher(
(callApmApi) => {
const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';
const { data: errorGroupListData } = useFetcher(() => {
const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';

if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors',
params: {
path: {
serviceName,
},
query: {
start,
end,
sortField,
sortDirection: normalizedSortDirection,
uiFilters: JSON.stringify(uiFilters),
},
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors',
params: {
path: {
serviceName,
},
});
}
},
[serviceName, start, end, sortField, sortDirection, uiFilters]
);
query: {
start,
end,
sortField,
sortDirection: normalizedSortDirection,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, sortField, sortDirection, uiFilters]);

useTrackPageview({
app: 'apm',
Expand Down Expand Up @@ -102,20 +99,27 @@ const ErrorGroupOverview: React.FC = () => {
<LocalUIFilters {...localUIFiltersConfig} />
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiPanel>
<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
{
defaultMessage: 'Error occurrences',
}
)}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<ChartsSyncContextProvider>
<EuiFlexItem>
<EuiPanel>
<ErrorDistribution
distribution={errorDistributionData}
title={i18n.translate(
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
{
defaultMessage: 'Error occurrences',
}
)}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>
<ErrorRateChart />
</EuiPanel>
</EuiFlexItem>
</ChartsSyncContextProvider>
</EuiFlexGroup>

<EuiSpacer size="s" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ export default function Legends({
return (
<Legend
key={i}
onClick={() => clickLegend(i)}
onClick={
serie.legendClickDisabled ? undefined : () => clickLegend(i)
}
disabled={seriesEnabledState[i]}
text={text}
color={serie.color}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class InnerCustomPlot extends PureComponent {
const hasValidCoordinates = flatten(series.map((s) => s.data)).some((p) =>
isValidCoordinateValue(p.y)
);
const noHits = !hasValidCoordinates;
const noHits = this.props.noHits || !hasValidCoordinates;

const plotValues = this.getPlotValues({
visibleSeries,
Expand Down Expand Up @@ -234,13 +234,16 @@ InnerCustomPlot.propTypes = {
firstSeen: PropTypes.number,
})
),
noHits: PropTypes.bool,
};

InnerCustomPlot.defaultProps = {
formatTooltipValue: (p) => p.y,
tickFormatX: undefined,
tickFormatY: (y) => y,
truncateLegends: false,
xAxisTickSizeOuter: 0,
noHits: false,
};

export default makeWidthFlexible(InnerCustomPlot);
Loading

0 comments on commit e65163b

Please sign in to comment.