From 581f5f72a709bdd30f824a6265e0005740fb87c9 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Fri, 15 May 2020 13:38:40 +0200 Subject: [PATCH 01/15] creating error rate chart --- .../app/ErrorGroupDetails/index.tsx | 93 ++++++------ .../app/ErrorGroupOverview/index.tsx | 138 ++++++++++++------ .../apm/server/lib/errors/get_error_rate.ts | 95 ++++++++++++ .../apm/server/routes/create_apm_api.ts | 16 +- x-pack/plugins/apm/server/routes/errors.ts | 24 +++ 5 files changed, 262 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/errors/get_error_rate.ts diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index d8885ec11c511..0d376abf3becd 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -11,7 +11,7 @@ import { EuiPanel, EuiSpacer, EuiText, - EuiTitle, + EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; @@ -26,6 +26,7 @@ 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'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; @@ -61,49 +62,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]); + + const { data: errorDistributionData } = useFetcher(() => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName }, - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); + 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 }); @@ -130,8 +125,8 @@ export function ErrorGroupDetails() { {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { defaultMessage: 'Error group {errorGroupId}', values: { - errorGroupId: getShortGroupId(urlParams.errorGroupId), - }, + errorGroupId: getShortGroupId(urlParams.errorGroupId) + } })} @@ -140,7 +135,7 @@ export function ErrorGroupDetails() { {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { - defaultMessage: 'Unhandled', + defaultMessage: 'Unhandled' })} @@ -160,7 +155,7 @@ export function ErrorGroupDetails() { {i18n.translate( 'xpack.apm.errorGroupDetails.logMessageLabel', { - defaultMessage: 'Log message', + defaultMessage: 'Log message' } )} @@ -171,14 +166,14 @@ export function ErrorGroupDetails() { {i18n.translate( 'xpack.apm.errorGroupDetails.exceptionMessageLabel', { - defaultMessage: 'Exception message', + defaultMessage: 'Exception message' } )} {excMessage || NOT_AVAILABLE_LABEL} {culprit || NOT_AVAILABLE_LABEL} @@ -191,7 +186,7 @@ export function ErrorGroupDetails() { title={i18n.translate( 'xpack.apm.errorGroupDetails.occurrencesChartLabel', { - defaultMessage: 'Occurrences', + defaultMessage: 'Occurrences' } )} /> diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index ff031c5a86d11..3f37892f39a83 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -9,7 +9,7 @@ import { EuiFlexItem, EuiPanel, EuiSpacer, - EuiTitle, + EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; @@ -20,61 +20,76 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import CustomPlot from '../../shared/charts/CustomPlot'; 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'; - - 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), - }, + const { data: erroRateData } = useFetcher(() => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/rate', + params: { + path: { + serviceName }, - }); - } - }, - [serviceName, start, end, sortField, sortDirection, uiFilters] - ); + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, [serviceName, start, end, uiFilters]); + console.log('### caue: erroRateData', erroRateData); + + 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) + } + } + }); + } + }, [serviceName, start, end, sortField, sortDirection, uiFilters]); useTrackPageview({ app: 'apm', - path: 'error_group_overview', + path: 'error_group_overview' }); useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); @@ -82,9 +97,9 @@ const ErrorGroupOverview: React.FC = () => { const config: React.ComponentProps = { filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], params: { - serviceName, + serviceName }, - projection: PROJECTION.ERROR_GROUPS, + projection: PROJECTION.ERROR_GROUPS }; return config; @@ -101,7 +116,7 @@ const ErrorGroupOverview: React.FC = () => { - + @@ -110,12 +125,39 @@ const ErrorGroupOverview: React.FC = () => { title={i18n.translate( 'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle', { - defaultMessage: 'Error occurrences', + defaultMessage: 'Error occurrences' } )} /> + + + {/* + {tpmLabel(transactionType)} */} + {/* + */} + {erroRateData && ( + + )} + + diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts new file mode 100644 index 0000000000000..245187933ddc8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -0,0 +1,95 @@ +/* + * 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 { ESFilter } from '../../../typings/elasticsearch'; +import { + SERVICE_NAME, + PROCESSOR_EVENT, + ERROR_GROUP_ID +} from '../../../common/elasticsearch_fieldnames'; +import { + Setup, + SetupTimeRange, + SetupUIFilters +} from '../helpers/setup_request'; +import { rangeFilter } from '../helpers/range_filter'; +import { BUCKET_TARGET_COUNT } from '../transactions/constants'; + +// TODO: refactor +function getBucketSize({ start, end }: SetupTimeRange) { + return Math.floor((end - start) / BUCKET_TARGET_COUNT); +} + +export async function getErrorRate({ + serviceName, + groupId, + setup +}: { + serviceName: string; + groupId?: string; + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES, client, indices } = setup; + const bucketSize = getBucketSize({ start, end }); + const filter: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + { + bool: { + should: [ + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: 'error' } } + ] + } + }, + ...uiFiltersES + ]; + + if (groupId) { + filter.push({ term: { [ERROR_GROUP_ID]: groupId } }); + } + + const aggs = { + count: { + histogram: { + field: '@timestamp', + min_doc_count: 0, + interval: bucketSize + }, + aggs: { + processorEventCount: { + terms: { + field: PROCESSOR_EVENT, + size: 10 + } + } + } + } + }; + + const params = { + index: [ + indices['apm_oss.errorIndices'], + indices['apm_oss.transactionIndices'] + ], + body: { + size: 0, + query: { bool: { filter } }, + aggs + } + }; + + const resp = await client.search(params); + return resp.aggregations?.count.buckets.map(histogram => { + const transactionCount = + histogram.processorEventCount.buckets.find( + event => event.key === 'transaction' + )?.doc_count || 1; + const errorCount = + histogram.processorEventCount.buckets.find(event => event.key === 'error') + ?.doc_count || 0; + return { x: histogram.key, y: (errorCount * 100) / transactionCount }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 774f1f27435a2..72a0885da3c59 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -7,12 +7,13 @@ import { staticIndexPatternRoute, dynamicIndexPatternRoute, - apmIndexPatternTitleRoute, + apmIndexPatternTitleRoute } from './index_pattern'; import { errorDistributionRoute, errorGroupsRoute, errorsRoute, + errorRateRoute } from './errors'; import { serviceAgentNameRoute, @@ -20,7 +21,7 @@ import { servicesRoute, serviceNodeMetadataRoute, serviceAnnotationsRoute, - serviceAnnotationsCreateRoute, + serviceAnnotationsCreateRoute } from './services'; import { agentConfigurationRoute, @@ -30,12 +31,12 @@ import { listAgentConfigurationEnvironmentsRoute, listAgentConfigurationServicesRoute, createOrUpdateAgentConfigurationRoute, - agentConfigurationAgentNameRoute, + agentConfigurationAgentNameRoute } from './settings/agent_configuration'; import { apmIndexSettingsRoute, apmIndicesRoute, - saveApmIndicesRoute, + saveApmIndicesRoute } from './settings/apm_indices'; import { metricsChartsRoute } from './metrics'; import { serviceNodesRoute } from './service_nodes'; @@ -47,7 +48,7 @@ import { transactionGroupsDistributionRoute, transactionGroupsRoute, transactionGroupsAvgDurationByCountry, - transactionGroupsAvgDurationByBrowser, + transactionGroupsAvgDurationByBrowser } from './transaction_groups'; import { errorGroupsLocalFiltersRoute, @@ -57,7 +58,7 @@ import { transactionGroupsLocalFiltersRoute, transactionsLocalFiltersRoute, serviceNodesLocalFiltersRoute, - uiFiltersEnvironmentsRoute, + uiFiltersEnvironmentsRoute } from './ui_filters'; import { createApi } from './create_api'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; @@ -67,7 +68,7 @@ import { updateCustomLinkRoute, deleteCustomLinkRoute, listCustomLinksRoute, - customLinkTransactionRoute, + customLinkTransactionRoute } from './settings/custom_link'; const createApmApi = () => { @@ -81,6 +82,7 @@ const createApmApi = () => { .add(errorDistributionRoute) .add(errorGroupsRoute) .add(errorsRoute) + .add(errorRateRoute) // Services .add(serviceAgentNameRoute) diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 7e45f412d4bdb..be84db0ff8539 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -11,6 +11,7 @@ import { getErrorGroup } from '../lib/errors/get_error_group'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; import { uiFiltersRt, rangeRt } from './default_api_types'; +import { getErrorRate } from '../lib/errors/get_error_rate'; export const errorsRoute = createRoute((core) => ({ path: '/api/apm/services/{serviceName}/errors', @@ -80,3 +81,26 @@ export const errorDistributionRoute = createRoute(() => ({ return getErrorDistribution({ serviceName, groupId, setup }); }, })); + +export const errorRateRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/errors/rate', + params: { + path: t.type({ + serviceName: t.string + }), + query: t.intersection([ + t.partial({ + groupId: t.string + }), + uiFiltersRt, + rangeRt + ]) + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + const { serviceName } = params.path; + const { groupId } = params.query; + return getErrorRate({ serviceName, groupId, setup }); + } +})); From 62e4a553a8d819639777db0efebfc0d2677566d2 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 19 May 2020 16:54:22 +0200 Subject: [PATCH 02/15] adding error line chart --- .../app/ErrorGroupOverview/index.tsx | 58 ++++++++++++------- .../apm/server/lib/errors/get_error_rate.ts | 26 ++++++--- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 3f37892f39a83..7aab4c412788f 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -11,8 +11,10 @@ import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; +import { Maybe } from '../../../../typings/common'; import { useFetcher } from '../../../hooks/useFetcher'; import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; @@ -21,10 +23,14 @@ import { useTrackPageview } from '../../../../../observability/public'; import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { callApmApi } from '../../../services/rest/createCallApmApi'; +// @ts-ignore import CustomPlot from '../../shared/charts/CustomPlot'; +import { unit } from '../../../style/variables'; +import { asPercent } from '../../../utils/formatters'; const ErrorGroupOverview: React.FC = () => { const { urlParams, uiFilters } = useUrlParams(); + const [errorRateTime, setErrorRateTime] = useState(); const { serviceName, start, end, sortField, sortDirection } = urlParams; @@ -63,7 +69,6 @@ const ErrorGroupOverview: React.FC = () => { }); } }, [serviceName, start, end, uiFilters]); - console.log('### caue: erroRateData', erroRateData); const { data: errorGroupListData } = useFetcher(() => { const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; @@ -109,6 +114,10 @@ const ErrorGroupOverview: React.FC = () => { return null; } + const tickFormatY = (y: Maybe) => { + return numeral(y || 0).format('0 %'); + }; + return ( <> @@ -116,7 +125,7 @@ const ErrorGroupOverview: React.FC = () => { - + @@ -133,27 +142,36 @@ const ErrorGroupOverview: React.FC = () => { - {/* - {tpmLabel(transactionType)} */} - {/* - */} + + + {i18n.translate( + 'xpack.apm.serviceDetails.metrics.errorRateChartTitle', + { + defaultMessage: 'Error Rate' + } + )} + + {erroRateData && ( { + setErrorRateTime(time); + }} + hoverX={errorRateTime} + tickFormatY={tickFormatY} + formatTooltipValue={({ y }: { y: number }) => { + return asPercent(y, 1); + }} + height={unit * 10} /> )} diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index 245187933ddc8..b22d985ed7b55 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -56,7 +56,11 @@ export async function getErrorRate({ histogram: { field: '@timestamp', min_doc_count: 0, - interval: bucketSize + interval: bucketSize, + extended_bounds: { + min: start, + max: end + } }, aggs: { processorEventCount: { @@ -83,13 +87,17 @@ export async function getErrorRate({ const resp = await client.search(params); return resp.aggregations?.count.buckets.map(histogram => { - const transactionCount = - histogram.processorEventCount.buckets.find( - event => event.key === 'transaction' - )?.doc_count || 1; - const errorCount = - histogram.processorEventCount.buckets.find(event => event.key === 'error') - ?.doc_count || 0; - return { x: histogram.key, y: (errorCount * 100) / transactionCount }; + const { + transaction: transactionCount = 1, + error: errorCount = 0 + } = histogram.processorEventCount.buckets.reduce( + (acc, { key, doc_count }) => ({ ...acc, [key]: doc_count }), + {} as { transaction?: number; error?: number } + ); + + return { + x: histogram.key, + y: errorCount / transactionCount + }; }); } From 43ec10d12c3a11bc0cc06bf1c20ea3c39b5feecf Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 25 May 2020 17:27:40 +0200 Subject: [PATCH 03/15] creating error rate chart --- .../ErrorGroupDetails/Distribution/index.tsx | 4 + .../app/ErrorGroupDetails/index.tsx | 30 +++-- .../app/ErrorGroupOverview/index.tsx | 104 ++++-------------- .../shared/charts/ErrorRateChart/index.tsx | 83 ++++++++++++++ .../shared/charts/Histogram/index.js | 84 +++++++++----- .../apm/server/lib/errors/get_error_rate.ts | 12 +- 6 files changed, 194 insertions(+), 123 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 796f2992236f9..590ea9b342212 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { scaleUtc } from 'd3-scale'; import d3 from 'd3'; import React from 'react'; +import { useChartsSync } from '../../../../hooks/useChartsSync'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; // @ts-ignore @@ -59,6 +60,8 @@ const tooltipHeader = (bucket: FormattedBucket) => asRelativeDateTimeRange(bucket.x0, bucket.x); export function ErrorDistribution({ distribution, title }: Props) { + const syncedChartsProps = useChartsSync(); + const buckets = getFormattedBuckets( distribution.buckets, distribution.bucketSize @@ -84,6 +87,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time-utc" diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 0d376abf3becd..f1a96cee6dc90 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -27,6 +27,8 @@ 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)}; @@ -180,16 +182,24 @@ export function ErrorGroupDetails() { )} - - + + + + + + + + + + {showDetails && ( diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 7aab4c412788f..432404e014520 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -11,26 +11,21 @@ import { EuiSpacer, EuiTitle } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import React, { useMemo, useState } from 'react'; -import { Maybe } from '../../../../typings/common'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; -import { ErrorGroupList } from './List'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { PROJECTION } from '../../../../common/projections/typings'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; -// @ts-ignore -import CustomPlot from '../../shared/charts/CustomPlot'; -import { unit } from '../../../style/variables'; -import { asPercent } from '../../../utils/formatters'; +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 [errorRateTime, setErrorRateTime] = useState(); const { serviceName, start, end, sortField, sortDirection } = urlParams; @@ -52,24 +47,6 @@ const ErrorGroupOverview: React.FC = () => { } }, [serviceName, start, end, uiFilters]); - const { data: erroRateData } = useFetcher(() => { - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/rate', - params: { - path: { - serviceName - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - } - }, [serviceName, start, end, uiFilters]); - const { data: errorGroupListData } = useFetcher(() => { const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; @@ -114,10 +91,6 @@ const ErrorGroupOverview: React.FC = () => { return null; } - const tickFormatY = (y: Maybe) => { - return numeral(y || 0).format('0 %'); - }; - return ( <> @@ -127,55 +100,26 @@ const ErrorGroupOverview: React.FC = () => { - - - - - - - - - - {i18n.translate( - 'xpack.apm.serviceDetails.metrics.errorRateChartTitle', + + + + - - {erroRateData && ( - { - setErrorRateTime(time); - }} - hoverX={errorRateTime} - tickFormatY={tickFormatY} - formatTooltipValue={({ y }: { y: number }) => { - return asPercent(y, 1); - }} - height={unit * 10} /> - )} - - + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx new file mode 100644 index 0000000000000..783a2c620588d --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -0,0 +1,83 @@ +/* + * 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 { EuiTitle } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback } from 'react'; +import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { unit } from '../../../../style/variables'; +import { asPercent } from '../../../../utils/formatters'; +// @ts-ignore +import CustomPlot from '../CustomPlot'; + +const tickFormatY = (y?: number) => { + return numeral(y || 0).format('0 %'); +}; + +export const ErrorRateChart = () => { + const { urlParams, uiFilters } = useUrlParams(); + const syncedChartsProps = useChartsSync(); + + const { serviceName, start, end, errorGroupId } = urlParams; + const { data: errorRateData = [] } = useFetcher(() => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/rate', + params: { + path: { + serviceName + }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + groupId: errorGroupId + } + } + }); + } + }, [serviceName, start, end, uiFilters, errorGroupId]); + + const combinedOnHover = useCallback( + (hoverX: number) => { + return syncedChartsProps.onHover(hoverX); + }, + [syncedChartsProps] + ); + + return ( + <> + + + {i18n.translate('xpack.apm.errorRateChart.title', { + defaultMessage: 'Error Rate' + })} + + + { + return asPercent(y, 1); + }} + height={unit * 10} + /> + + ); +}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index 4eca1a37c51bc..26491a1ae7762 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -19,7 +19,7 @@ import { VerticalRectSeries, Voronoi, makeWidthFlexible, - VerticalGridLines, + VerticalGridLines } from 'react-vis'; import { unit } from '../../../../style/variables'; import Tooltip from '../Tooltip'; @@ -32,7 +32,7 @@ const XY_MARGIN = { top: unit, left: unit * 5, right: unit, - bottom: unit * 2, + bottom: unit * 2 }; const X_TICK_TOTAL = 8; @@ -45,33 +45,58 @@ const ChartsWrapper = styled.div` left: 0; `; +function findBucket(buckets, xAxisValue) { + return buckets.find(bucket => bucket.x === xAxisValue); +} + export class HistogramInner extends PureComponent { constructor(props) { super(props); this.state = { - hoveredBucket: {}, + hoveredBucket: {} }; } - onClick = (bucket) => { + componentDidUpdate(prevProps) { + if (prevProps.hoverX !== this.props.hoverX) { + const bucketFound = findBucket(this.props.buckets, this.props.hoverX); + if (bucketFound) { + this.setState({ + hoveredBucket: { + ...bucketFound, + xCenter: (bucketFound.x0 + bucketFound.x) / 2 + } + }); + } + } + } + + onClick = bucket => { if (this.props.onClick) { this.props.onClick(bucket); } }; - onHover = (bucket) => { - this.setState({ hoveredBucket: bucket }); + onHover = bucket => { + if (this.props.onHover) { + this.props.onHover(bucket.x); + } else { + this.setState({ hoveredBucket: bucket }); + } }; onBlur = () => { + if (this.props.onMouseLeave) { + this.props.onMouseLeave(); + } this.setState({ hoveredBucket: {} }); }; getChartData(items, selectedItem) { - const yMax = d3.max(items, (d) => d.y); + const yMax = d3.max(items, d => d.y); const MINIMUM_BUCKET_SIZE = yMax * 0.02; - return items.map((item) => { + return items.map(item => { const padding = (item.x - item.x0) / 20; return { ...item, @@ -81,7 +106,7 @@ export class HistogramInner extends PureComponent { : tint(0.5, theme.euiColorVis1), x0: item.x0 + padding, x: item.x - padding, - y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0, + y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0 }; }); } @@ -98,9 +123,9 @@ export class HistogramInner extends PureComponent { tooltipFooter, tooltipHeader, verticalLineHover, - width: XY_WIDTH, + width: XY_WIDTH } = this.props; - const { hoveredBucket } = this.state; + const { hoveredBucket = {} } = this.state; if (isEmpty(buckets) || XY_WIDTH === 0) { return null; } @@ -108,10 +133,10 @@ export class HistogramInner extends PureComponent { const isTimeSeries = this.props.xType === 'time' || this.props.xType === 'time-utc'; - const xMin = d3.min(buckets, (d) => d.x0); - const xMax = d3.max(buckets, (d) => d.x); + const xMin = d3.min(buckets, d => d.x0); + const xMax = d3.max(buckets, d => d.x); const yMin = 0; - const yMax = d3.max(buckets, (d) => d.y); + const yMax = d3.max(buckets, d => d.y); const selectedBucket = buckets[bucketIndex]; const chartData = this.getChartData(buckets, selectedBucket); @@ -119,14 +144,17 @@ export class HistogramInner extends PureComponent { .domain([xMin, xMax]) .range([XY_MARGIN.left, XY_WIDTH - XY_MARGIN.right]); - const y = scaleLinear().domain([yMin, yMax]).range([XY_HEIGHT, 0]).nice(); + const y = scaleLinear() + .domain([yMin, yMax]) + .range([XY_HEIGHT, 0]) + .nice(); const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax); const xTickValues = isTimeSeries ? getTimeTicksTZ({ domain: [xMinZone, xMaxZone], totalTicks: X_TICK_TOTAL, - width: XY_WIDTH, + width: XY_WIDTH }) : undefined; @@ -172,7 +200,7 @@ export class HistogramInner extends PureComponent { x={x(hoveredBucket.x0)} width={x(bucketSize) - x(0)} style={{ - fill: theme.euiColorLightestShade, + fill: theme.euiColorLightestShade }} /> )} @@ -181,7 +209,7 @@ export class HistogramInner extends PureComponent { )} @@ -209,7 +237,7 @@ export class HistogramInner extends PureComponent { data={chartData} style={{ rx: '0px', - ry: '0px', + ry: '0px' }} /> @@ -220,18 +248,18 @@ export class HistogramInner extends PureComponent { { + nodes={buckets.map(bucket => { return { ...bucket, - xCenter: (bucket.x0 + bucket.x) / 2, + xCenter: (bucket.x0 + bucket.x) / 2 }; })} onClick={this.onClick} onHover={this.onHover} onBlur={this.onBlur} - x={(d) => x(d.xCenter)} + x={d => x(d.xCenter)} y={() => 1} /> @@ -254,17 +282,17 @@ HistogramInner.propTypes = { tooltipHeader: PropTypes.func, verticalLineHover: PropTypes.func, width: PropTypes.number.isRequired, - xType: PropTypes.string, + xType: PropTypes.string }; HistogramInner.defaultProps = { backgroundHover: () => null, - formatYLong: (value) => value, - formatYShort: (value) => value, + formatYLong: value => value, + formatYShort: value => value, tooltipFooter: () => null, tooltipHeader: () => null, verticalLineHover: () => null, - xType: 'linear', + xType: 'linear' }; export default makeWidthFlexible(HistogramInner); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index b22d985ed7b55..a37f63ec5c33e 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -33,6 +33,8 @@ export async function getErrorRate({ }) { const { start, end, uiFiltersES, client, indices } = setup; const bucketSize = getBucketSize({ start, end }); + const groupIdTerm = groupId ? [{ term: { [ERROR_GROUP_ID]: groupId } }] : []; + const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, @@ -40,17 +42,17 @@ export async function getErrorRate({ bool: { should: [ { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { term: { [PROCESSOR_EVENT]: 'error' } } + { + bool: { + filter: [{ term: { [PROCESSOR_EVENT]: 'error' } }, ...groupIdTerm] + } + } ] } }, ...uiFiltersES ]; - if (groupId) { - filter.push({ term: { [ERROR_GROUP_ID]: groupId } }); - } - const aggs = { count: { histogram: { From 9e59aca14a62fd0ce691675c6f9a5841af987ea5 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 26 May 2020 15:17:14 +0200 Subject: [PATCH 04/15] using date_histogram --- .../shared/charts/ErrorRateChart/index.tsx | 2 +- .../shared/charts/Histogram/index.js | 2 ++ .../apm/server/lib/errors/get_error_rate.ts | 26 +++++++------------ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 783a2c620588d..6eaac23443340 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -6,7 +6,7 @@ import { EuiTitle } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import React, { useState, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index 26491a1ae7762..7a98bafb207a2 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -67,6 +67,8 @@ export class HistogramInner extends PureComponent { xCenter: (bucketFound.x0 + bucketFound.x) / 2 } }); + } else { + this.setState({ hoveredBucket: {} }); } } } diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index a37f63ec5c33e..e167c1a615454 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -15,12 +15,7 @@ import { SetupUIFilters } from '../helpers/setup_request'; import { rangeFilter } from '../helpers/range_filter'; -import { BUCKET_TARGET_COUNT } from '../transactions/constants'; - -// TODO: refactor -function getBucketSize({ start, end }: SetupTimeRange) { - return Math.floor((end - start) / BUCKET_TARGET_COUNT); -} +import { getBucketSize } from '../helpers/get_bucket_size'; export async function getErrorRate({ serviceName, @@ -32,7 +27,7 @@ export async function getErrorRate({ setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, client, indices } = setup; - const bucketSize = getBucketSize({ start, end }); + const { intervalString } = getBucketSize(start, end, 'auto'); const groupIdTerm = groupId ? [{ term: { [ERROR_GROUP_ID]: groupId } }] : []; const filter: ESFilter[] = [ @@ -54,15 +49,12 @@ export async function getErrorRate({ ]; const aggs = { - count: { - histogram: { + response_times: { + date_histogram: { field: '@timestamp', + fixed_interval: intervalString, min_doc_count: 0, - interval: bucketSize, - extended_bounds: { - min: start, - max: end - } + extended_bounds: { min: start, max: end } }, aggs: { processorEventCount: { @@ -88,17 +80,17 @@ export async function getErrorRate({ }; const resp = await client.search(params); - return resp.aggregations?.count.buckets.map(histogram => { + return resp.aggregations?.response_times.buckets.map(responseTime => { const { transaction: transactionCount = 1, error: errorCount = 0 - } = histogram.processorEventCount.buckets.reduce( + } = responseTime.processorEventCount.buckets.reduce( (acc, { key, doc_count }) => ({ ...acc, [key]: doc_count }), {} as { transaction?: number; error?: number } ); return { - x: histogram.key, + x: responseTime.key, y: errorCount / transactionCount }; }); From 517bec6a6f4fb5dd256a3c515a9f6bae03362482 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 26 May 2020 16:06:21 +0200 Subject: [PATCH 05/15] reapplying prettier style --- .../app/ErrorGroupDetails/index.tsx | 32 +++++----- .../app/ErrorGroupOverview/index.tsx | 26 ++++---- .../shared/charts/ErrorRateChart/index.tsx | 14 ++--- .../shared/charts/Histogram/index.js | 61 +++++++++---------- .../apm/server/lib/errors/get_error_rate.ts | 45 +++++++------- .../apm/server/routes/create_apm_api.ts | 16 ++--- x-pack/plugins/apm/server/routes/errors.ts | 10 +-- 7 files changed, 102 insertions(+), 102 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index f1a96cee6dc90..5559f5012abce 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -11,7 +11,7 @@ import { EuiPanel, EuiSpacer, EuiText, - EuiTitle + EuiTitle, } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; @@ -71,14 +71,14 @@ export function ErrorGroupDetails() { params: { path: { serviceName, - groupId: errorGroupId + groupId: errorGroupId, }, query: { start, end, - uiFilters: JSON.stringify(uiFilters) - } - } + uiFilters: JSON.stringify(uiFilters), + }, + }, }); } }, [serviceName, start, end, errorGroupId, uiFilters]); @@ -89,15 +89,15 @@ export function ErrorGroupDetails() { pathname: '/api/apm/services/{serviceName}/errors/distribution', params: { path: { - serviceName + serviceName, }, query: { start, end, groupId: errorGroupId, - uiFilters: JSON.stringify(uiFilters) - } - } + uiFilters: JSON.stringify(uiFilters), + }, + }, }); } }, [serviceName, start, end, errorGroupId, uiFilters]); @@ -127,8 +127,8 @@ export function ErrorGroupDetails() { {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { defaultMessage: 'Error group {errorGroupId}', values: { - errorGroupId: getShortGroupId(urlParams.errorGroupId) - } + errorGroupId: getShortGroupId(urlParams.errorGroupId), + }, })} @@ -137,7 +137,7 @@ export function ErrorGroupDetails() { {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { - defaultMessage: 'Unhandled' + defaultMessage: 'Unhandled', })} @@ -157,7 +157,7 @@ export function ErrorGroupDetails() { {i18n.translate( 'xpack.apm.errorGroupDetails.logMessageLabel', { - defaultMessage: 'Log message' + defaultMessage: 'Log message', } )} @@ -168,14 +168,14 @@ export function ErrorGroupDetails() { {i18n.translate( 'xpack.apm.errorGroupDetails.exceptionMessageLabel', { - defaultMessage: 'Exception message' + defaultMessage: 'Exception message', } )} {excMessage || NOT_AVAILABLE_LABEL} {culprit || NOT_AVAILABLE_LABEL} @@ -190,7 +190,7 @@ export function ErrorGroupDetails() { title={i18n.translate( 'xpack.apm.errorGroupDetails.occurrencesChartLabel', { - defaultMessage: 'Occurrences' + defaultMessage: 'Occurrences', } )} /> diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 432404e014520..acb95393d5715 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -9,7 +9,7 @@ import { EuiFlexItem, EuiPanel, EuiSpacer, - EuiTitle + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; @@ -35,14 +35,14 @@ const ErrorGroupOverview: React.FC = () => { pathname: '/api/apm/services/{serviceName}/errors/distribution', params: { path: { - serviceName + serviceName, }, query: { start, end, - uiFilters: JSON.stringify(uiFilters) - } - } + uiFilters: JSON.stringify(uiFilters), + }, + }, }); } }, [serviceName, start, end, uiFilters]); @@ -55,23 +55,23 @@ const ErrorGroupOverview: React.FC = () => { pathname: '/api/apm/services/{serviceName}/errors', params: { path: { - serviceName + serviceName, }, query: { start, end, sortField, sortDirection: normalizedSortDirection, - uiFilters: JSON.stringify(uiFilters) - } - } + uiFilters: JSON.stringify(uiFilters), + }, + }, }); } }, [serviceName, start, end, sortField, sortDirection, uiFilters]); useTrackPageview({ app: 'apm', - path: 'error_group_overview' + path: 'error_group_overview', }); useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); @@ -79,9 +79,9 @@ const ErrorGroupOverview: React.FC = () => { const config: React.ComponentProps = { filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], params: { - serviceName + serviceName, }, - projection: PROJECTION.ERROR_GROUPS + projection: PROJECTION.ERROR_GROUPS, }; return config; @@ -108,7 +108,7 @@ const ErrorGroupOverview: React.FC = () => { title={i18n.translate( 'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle', { - defaultMessage: 'Error occurrences' + defaultMessage: 'Error occurrences', } )} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 6eaac23443340..2b786508659fd 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -31,15 +31,15 @@ export const ErrorRateChart = () => { pathname: '/api/apm/services/{serviceName}/errors/rate', params: { path: { - serviceName + serviceName, }, query: { start, end, uiFilters: JSON.stringify(uiFilters), - groupId: errorGroupId - } - } + groupId: errorGroupId, + }, + }, }); } }, [serviceName, start, end, uiFilters, errorGroupId]); @@ -56,7 +56,7 @@ export const ErrorRateChart = () => { {i18n.translate('xpack.apm.errorRateChart.title', { - defaultMessage: 'Error Rate' + defaultMessage: 'Error Rate', })} @@ -68,8 +68,8 @@ export const ErrorRateChart = () => { type: 'line', color: '#f5a700', hideLegend: true, - title: 'Rate' - } + title: 'Rate', + }, ]} onHover={combinedOnHover} tickFormatY={tickFormatY} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index 7a98bafb207a2..ce94fa4dc6aa7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -19,7 +19,7 @@ import { VerticalRectSeries, Voronoi, makeWidthFlexible, - VerticalGridLines + VerticalGridLines, } from 'react-vis'; import { unit } from '../../../../style/variables'; import Tooltip from '../Tooltip'; @@ -32,7 +32,7 @@ const XY_MARGIN = { top: unit, left: unit * 5, right: unit, - bottom: unit * 2 + bottom: unit * 2, }; const X_TICK_TOTAL = 8; @@ -46,14 +46,14 @@ const ChartsWrapper = styled.div` `; function findBucket(buckets, xAxisValue) { - return buckets.find(bucket => bucket.x === xAxisValue); + return buckets.find((bucket) => bucket.x === xAxisValue); } export class HistogramInner extends PureComponent { constructor(props) { super(props); this.state = { - hoveredBucket: {} + hoveredBucket: {}, }; } @@ -64,8 +64,8 @@ export class HistogramInner extends PureComponent { this.setState({ hoveredBucket: { ...bucketFound, - xCenter: (bucketFound.x0 + bucketFound.x) / 2 - } + xCenter: (bucketFound.x0 + bucketFound.x) / 2, + }, }); } else { this.setState({ hoveredBucket: {} }); @@ -73,13 +73,13 @@ export class HistogramInner extends PureComponent { } } - onClick = bucket => { + onClick = (bucket) => { if (this.props.onClick) { this.props.onClick(bucket); } }; - onHover = bucket => { + onHover = (bucket) => { if (this.props.onHover) { this.props.onHover(bucket.x); } else { @@ -95,10 +95,10 @@ export class HistogramInner extends PureComponent { }; getChartData(items, selectedItem) { - const yMax = d3.max(items, d => d.y); + const yMax = d3.max(items, (d) => d.y); const MINIMUM_BUCKET_SIZE = yMax * 0.02; - return items.map(item => { + return items.map((item) => { const padding = (item.x - item.x0) / 20; return { ...item, @@ -108,7 +108,7 @@ export class HistogramInner extends PureComponent { : tint(0.5, theme.euiColorVis1), x0: item.x0 + padding, x: item.x - padding, - y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0 + y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0, }; }); } @@ -125,7 +125,7 @@ export class HistogramInner extends PureComponent { tooltipFooter, tooltipHeader, verticalLineHover, - width: XY_WIDTH + width: XY_WIDTH, } = this.props; const { hoveredBucket = {} } = this.state; if (isEmpty(buckets) || XY_WIDTH === 0) { @@ -135,10 +135,10 @@ export class HistogramInner extends PureComponent { const isTimeSeries = this.props.xType === 'time' || this.props.xType === 'time-utc'; - const xMin = d3.min(buckets, d => d.x0); - const xMax = d3.max(buckets, d => d.x); + const xMin = d3.min(buckets, (d) => d.x0); + const xMax = d3.max(buckets, (d) => d.x); const yMin = 0; - const yMax = d3.max(buckets, d => d.y); + const yMax = d3.max(buckets, (d) => d.y); const selectedBucket = buckets[bucketIndex]; const chartData = this.getChartData(buckets, selectedBucket); @@ -146,17 +146,14 @@ export class HistogramInner extends PureComponent { .domain([xMin, xMax]) .range([XY_MARGIN.left, XY_WIDTH - XY_MARGIN.right]); - const y = scaleLinear() - .domain([yMin, yMax]) - .range([XY_HEIGHT, 0]) - .nice(); + const y = scaleLinear().domain([yMin, yMax]).range([XY_HEIGHT, 0]).nice(); const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax); const xTickValues = isTimeSeries ? getTimeTicksTZ({ domain: [xMinZone, xMaxZone], totalTicks: X_TICK_TOTAL, - width: XY_WIDTH + width: XY_WIDTH, }) : undefined; @@ -202,7 +199,7 @@ export class HistogramInner extends PureComponent { x={x(hoveredBucket.x0)} width={x(bucketSize) - x(0)} style={{ - fill: theme.euiColorLightestShade + fill: theme.euiColorLightestShade, }} /> )} @@ -211,7 +208,7 @@ export class HistogramInner extends PureComponent { )} @@ -239,7 +236,7 @@ export class HistogramInner extends PureComponent { data={chartData} style={{ rx: '0px', - ry: '0px' + ry: '0px', }} /> @@ -250,18 +247,18 @@ export class HistogramInner extends PureComponent { { + nodes={buckets.map((bucket) => { return { ...bucket, - xCenter: (bucket.x0 + bucket.x) / 2 + xCenter: (bucket.x0 + bucket.x) / 2, }; })} onClick={this.onClick} onHover={this.onHover} onBlur={this.onBlur} - x={d => x(d.xCenter)} + x={(d) => x(d.xCenter)} y={() => 1} /> @@ -284,17 +281,17 @@ HistogramInner.propTypes = { tooltipHeader: PropTypes.func, verticalLineHover: PropTypes.func, width: PropTypes.number.isRequired, - xType: PropTypes.string + xType: PropTypes.string, }; HistogramInner.defaultProps = { backgroundHover: () => null, - formatYLong: value => value, - formatYShort: value => value, + formatYLong: (value) => value, + formatYShort: (value) => value, tooltipFooter: () => null, tooltipHeader: () => null, verticalLineHover: () => null, - xType: 'linear' + xType: 'linear', }; export default makeWidthFlexible(HistogramInner); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index e167c1a615454..53c89cb7209f9 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -7,12 +7,12 @@ import { ESFilter } from '../../../typings/elasticsearch'; import { SERVICE_NAME, PROCESSOR_EVENT, - ERROR_GROUP_ID + ERROR_GROUP_ID, } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange, - SetupUIFilters + SetupUIFilters, } from '../helpers/setup_request'; import { rangeFilter } from '../helpers/range_filter'; import { getBucketSize } from '../helpers/get_bucket_size'; @@ -20,7 +20,7 @@ import { getBucketSize } from '../helpers/get_bucket_size'; export async function getErrorRate({ serviceName, groupId, - setup + setup, }: { serviceName: string; groupId?: string; @@ -39,13 +39,16 @@ export async function getErrorRate({ { term: { [PROCESSOR_EVENT]: 'transaction' } }, { bool: { - filter: [{ term: { [PROCESSOR_EVENT]: 'error' } }, ...groupIdTerm] - } - } - ] - } + filter: [ + { term: { [PROCESSOR_EVENT]: 'error' } }, + ...groupIdTerm, + ], + }, + }, + ], + }, }, - ...uiFiltersES + ...uiFiltersES, ]; const aggs = { @@ -54,36 +57,36 @@ export async function getErrorRate({ field: '@timestamp', fixed_interval: intervalString, min_doc_count: 0, - extended_bounds: { min: start, max: end } + extended_bounds: { min: start, max: end }, }, aggs: { processorEventCount: { terms: { field: PROCESSOR_EVENT, - size: 10 - } - } - } - } + size: 10, + }, + }, + }, + }, }; const params = { index: [ indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'] + indices['apm_oss.transactionIndices'], ], body: { size: 0, query: { bool: { filter } }, - aggs - } + aggs, + }, }; const resp = await client.search(params); - return resp.aggregations?.response_times.buckets.map(responseTime => { + return resp.aggregations?.response_times.buckets.map((responseTime) => { const { transaction: transactionCount = 1, - error: errorCount = 0 + error: errorCount = 0, } = responseTime.processorEventCount.buckets.reduce( (acc, { key, doc_count }) => ({ ...acc, [key]: doc_count }), {} as { transaction?: number; error?: number } @@ -91,7 +94,7 @@ export async function getErrorRate({ return { x: responseTime.key, - y: errorCount / transactionCount + y: errorCount / transactionCount, }; }); } diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 72a0885da3c59..bdfb49fa30828 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -7,13 +7,13 @@ import { staticIndexPatternRoute, dynamicIndexPatternRoute, - apmIndexPatternTitleRoute + apmIndexPatternTitleRoute, } from './index_pattern'; import { errorDistributionRoute, errorGroupsRoute, errorsRoute, - errorRateRoute + errorRateRoute, } from './errors'; import { serviceAgentNameRoute, @@ -21,7 +21,7 @@ import { servicesRoute, serviceNodeMetadataRoute, serviceAnnotationsRoute, - serviceAnnotationsCreateRoute + serviceAnnotationsCreateRoute, } from './services'; import { agentConfigurationRoute, @@ -31,12 +31,12 @@ import { listAgentConfigurationEnvironmentsRoute, listAgentConfigurationServicesRoute, createOrUpdateAgentConfigurationRoute, - agentConfigurationAgentNameRoute + agentConfigurationAgentNameRoute, } from './settings/agent_configuration'; import { apmIndexSettingsRoute, apmIndicesRoute, - saveApmIndicesRoute + saveApmIndicesRoute, } from './settings/apm_indices'; import { metricsChartsRoute } from './metrics'; import { serviceNodesRoute } from './service_nodes'; @@ -48,7 +48,7 @@ import { transactionGroupsDistributionRoute, transactionGroupsRoute, transactionGroupsAvgDurationByCountry, - transactionGroupsAvgDurationByBrowser + transactionGroupsAvgDurationByBrowser, } from './transaction_groups'; import { errorGroupsLocalFiltersRoute, @@ -58,7 +58,7 @@ import { transactionGroupsLocalFiltersRoute, transactionsLocalFiltersRoute, serviceNodesLocalFiltersRoute, - uiFiltersEnvironmentsRoute + uiFiltersEnvironmentsRoute, } from './ui_filters'; import { createApi } from './create_api'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; @@ -68,7 +68,7 @@ import { updateCustomLinkRoute, deleteCustomLinkRoute, listCustomLinksRoute, - customLinkTransactionRoute + customLinkTransactionRoute, } from './settings/custom_link'; const createApmApi = () => { diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index be84db0ff8539..12803df264189 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -86,15 +86,15 @@ export const errorRateRoute = createRoute(() => ({ path: '/api/apm/services/{serviceName}/errors/rate', params: { path: t.type({ - serviceName: t.string + serviceName: t.string, }), query: t.intersection([ t.partial({ - groupId: t.string + groupId: t.string, }), uiFiltersRt, - rangeRt - ]) + rangeRt, + ]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -102,5 +102,5 @@ export const errorRateRoute = createRoute(() => ({ const { serviceName } = params.path; const { groupId } = params.query; return getErrorRate({ serviceName, groupId, setup }); - } + }, })); From 391883fa7a2416f35af54dce1bc4149654d182cb Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 26 May 2020 18:31:11 +0200 Subject: [PATCH 06/15] changing to theme color --- .../public/components/shared/charts/ErrorRateChart/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 2b786508659fd..84edf8c9af75e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import theme from '@elastic/eui/dist/eui_theme_light.json'; import { EuiTitle } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; @@ -66,7 +67,7 @@ export const ErrorRateChart = () => { { data: errorRateData, type: 'line', - color: '#f5a700', + color: theme.euiColorVis7, hideLegend: true, title: 'Rate', }, From 7bfbb7d149e2d9116fedcea1d57770cd67cea3c7 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 2 Jun 2020 15:54:41 +0200 Subject: [PATCH 07/15] dont sync tooltips --- .../ErrorGroupDetails/Distribution/index.tsx | 4 --- .../app/ErrorGroupOverview/index.tsx | 2 +- .../shared/charts/Histogram/index.js | 31 ++----------------- 3 files changed, 3 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 590ea9b342212..796f2992236f9 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { scaleUtc } from 'd3-scale'; import d3 from 'd3'; import React from 'react'; -import { useChartsSync } from '../../../../hooks/useChartsSync'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; // @ts-ignore @@ -60,8 +59,6 @@ const tooltipHeader = (bucket: FormattedBucket) => asRelativeDateTimeRange(bucket.x0, bucket.x); export function ErrorDistribution({ distribution, title }: Props) { - const syncedChartsProps = useChartsSync(); - const buckets = getFormattedBuckets( distribution.buckets, distribution.bucketSize @@ -87,7 +84,6 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time-utc" diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index acb95393d5715..73474208e26c0 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -99,7 +99,7 @@ const ErrorGroupOverview: React.FC = () => { - + diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index ce94fa4dc6aa7..4eca1a37c51bc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -45,10 +45,6 @@ const ChartsWrapper = styled.div` left: 0; `; -function findBucket(buckets, xAxisValue) { - return buckets.find((bucket) => bucket.x === xAxisValue); -} - export class HistogramInner extends PureComponent { constructor(props) { super(props); @@ -57,22 +53,6 @@ export class HistogramInner extends PureComponent { }; } - componentDidUpdate(prevProps) { - if (prevProps.hoverX !== this.props.hoverX) { - const bucketFound = findBucket(this.props.buckets, this.props.hoverX); - if (bucketFound) { - this.setState({ - hoveredBucket: { - ...bucketFound, - xCenter: (bucketFound.x0 + bucketFound.x) / 2, - }, - }); - } else { - this.setState({ hoveredBucket: {} }); - } - } - } - onClick = (bucket) => { if (this.props.onClick) { this.props.onClick(bucket); @@ -80,17 +60,10 @@ export class HistogramInner extends PureComponent { }; onHover = (bucket) => { - if (this.props.onHover) { - this.props.onHover(bucket.x); - } else { - this.setState({ hoveredBucket: bucket }); - } + this.setState({ hoveredBucket: bucket }); }; onBlur = () => { - if (this.props.onMouseLeave) { - this.props.onMouseLeave(); - } this.setState({ hoveredBucket: {} }); }; @@ -127,7 +100,7 @@ export class HistogramInner extends PureComponent { verticalLineHover, width: XY_WIDTH, } = this.props; - const { hoveredBucket = {} } = this.state; + const { hoveredBucket } = this.state; if (isEmpty(buckets) || XY_WIDTH === 0) { return null; } From 9bde4f893dc8c58cdfdabed894f1d4c69f1da10e Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Wed, 3 Jun 2020 12:44:00 +0200 Subject: [PATCH 08/15] adding avg on error charts --- .../ErrorGroupDetails/Distribution/index.tsx | 17 ++++++++++++++++- .../app/ErrorGroupDetails/index.tsx | 2 +- .../shared/charts/ErrorRateChart/index.tsx | 19 +++++++++++++++---- .../shared/charts/Histogram/index.js | 14 ++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 796f2992236f9..c8537fcdaec4c 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -5,10 +5,13 @@ */ import { EuiTitle } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { scaleUtc } from 'd3-scale'; +import mean from 'lodash.mean'; import d3 from 'd3'; -import React from 'react'; +import React, { useMemo } from 'react'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; // @ts-ignore @@ -64,6 +67,11 @@ export function ErrorDistribution({ distribution, title }: Props) { distribution.bucketSize ); + const average = useMemo(() => { + const averageValue = buckets ? mean(buckets.map((bucket) => bucket.y)) : 0; + return numeral(averageValue).format('0a'); + }, [buckets]); + if (!buckets || distribution.noHits) { return ( ); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 5559f5012abce..225e5ef2f6ca2 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -182,7 +182,7 @@ export function ErrorGroupDetails() { )} - + { - return numeral(y || 0).format('0 %'); + return asPercent(y || 0, 1); }; export const ErrorRateChart = () => { @@ -45,6 +45,10 @@ export const ErrorRateChart = () => { } }, [serviceName, start, end, uiFilters, errorGroupId]); + const average = useMemo(() => { + return tickFormatY(mean(errorRateData.map((rate) => rate.y))); + }, [errorRateData]); + const combinedOnHover = useCallback( (hoverX: number) => { return syncedChartsProps.onHover(hoverX); @@ -64,6 +68,13 @@ export const ErrorRateChart = () => { 1} /> + + {legends && ( + {}} + truncateLegends={false} + noHits={false} + /> + )} ); @@ -255,6 +268,7 @@ HistogramInner.propTypes = { verticalLineHover: PropTypes.func, width: PropTypes.number.isRequired, xType: PropTypes.string, + legends: PropTypes.array, }; HistogramInner.defaultProps = { From 34f00007c48c66b88faf9cc3c4ad118fa870a9eb Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Fri, 5 Jun 2020 17:57:37 +0200 Subject: [PATCH 09/15] addressing pr comments --- .../shared/charts/CustomPlot/StaticPlot.js | 12 +- .../shared/charts/CustomPlot/index.js | 11 +- .../shared/charts/ErrorRateChart/index.tsx | 2 + .../apm/server/lib/errors/get_error_rate.ts | 105 +++++++++--------- 4 files changed, 77 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js index d489970b55f29..0ae969e898cb7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js @@ -158,7 +158,13 @@ class StaticPlot extends PureComponent { }; render() { - const { series, tickFormatY, plotValues, noHits } = this.props; + const { + series, + tickFormatY, + plotValues, + noHits, + xAxisTickSizeOuter = 0, + } = this.props; const { xTickValues, yTickValues } = plotValues; const tickFormatX = this.props.tickFormatX || this.tickFormatXTime; @@ -167,9 +173,10 @@ class StaticPlot extends PureComponent { {noHits ? ( {this.state.showAnnotations && !isEmpty(annotations) && ( @@ -227,6 +234,7 @@ InnerCustomPlot.propTypes = { width: PropTypes.number.isRequired, height: PropTypes.number, stackBy: PropTypes.string, + xAxisTickSizeOuter: PropTypes.number, annotations: PropTypes.arrayOf( PropTypes.shape({ type: PropTypes.string, @@ -241,6 +249,7 @@ InnerCustomPlot.defaultProps = { tickFormatX: undefined, tickFormatY: (y) => y, truncateLegends: false, + xAxisTickSizeOuter: 0, }; export default makeWidthFlexible(InnerCustomPlot); diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 778f0bb8ebf00..6f0a4798d0204 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -74,6 +74,7 @@ export const ErrorRateChart = () => { legendValue: average, title: 'Avg.', type: 'linemark', + hideTooltipValue: true, }, { data: errorRateData, @@ -89,6 +90,7 @@ export const ErrorRateChart = () => { return asPercent(y, 1); }} height={unit * 10} + xAxisTickSizeOuter={10} /> ); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index 53c89cb7209f9..2301011022938 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -3,19 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../typings/elasticsearch'; +import { ProcessorEvent } from '../../../common/processor_event'; import { + ERROR_GROUP_ID, SERVICE_NAME, PROCESSOR_EVENT, - ERROR_GROUP_ID, } from '../../../common/elasticsearch_fieldnames'; +import { getBucketSize } from '../helpers/get_bucket_size'; +import { rangeFilter } from '../helpers/range_filter'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { rangeFilter } from '../helpers/range_filter'; -import { getBucketSize } from '../helpers/get_bucket_size'; export async function getErrorRate({ serviceName, @@ -30,24 +30,9 @@ export async function getErrorRate({ const { intervalString } = getBucketSize(start, end, 'auto'); const groupIdTerm = groupId ? [{ term: { [ERROR_GROUP_ID]: groupId } }] : []; - const filter: ESFilter[] = [ + const filter = [ { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, - { - bool: { - should: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { - bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: 'error' } }, - ...groupIdTerm, - ], - }, - }, - ], - }, - }, ...uiFiltersES, ]; @@ -59,42 +44,62 @@ export async function getErrorRate({ min_doc_count: 0, extended_bounds: { min: start, max: end }, }, - aggs: { - processorEventCount: { - terms: { - field: PROCESSOR_EVENT, - size: 10, + }, + }; + + const getTransactionBucketAggregation = async () => { + const resp = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...filter, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ], }, }, + aggs, }, - }, + }); + return resp.aggregations?.response_times.buckets; }; - - const params = { - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], - body: { - size: 0, - query: { bool: { filter } }, - aggs, - }, + const getErrorBucketAggregation = async () => { + const resp = await client.search({ + index: indices['apm_oss.errorIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...filter, + ...groupIdTerm, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + ], + }, + }, + aggs, + }, + }); + return resp.aggregations?.response_times.buckets; }; - const resp = await client.search(params); - return resp.aggregations?.response_times.buckets.map((responseTime) => { - const { - transaction: transactionCount = 1, - error: errorCount = 0, - } = responseTime.processorEventCount.buckets.reduce( - (acc, { key, doc_count }) => ({ ...acc, [key]: doc_count }), - {} as { transaction?: number; error?: number } - ); + // Needed to the wrap the call to client.search in a function to avoid "Type instantiation is excessively deep and possibly infinite" + const [transactionsCount, errorsCount] = await Promise.all([ + getTransactionBucketAggregation(), + getErrorBucketAggregation(), + ]); + + const transactionByTimestamp: Record = {}; + if (transactionsCount) { + transactionsCount.forEach((bucket) => { + transactionByTimestamp[bucket.key] = bucket.doc_count; + }); + } - return { - x: responseTime.key, - y: errorCount / transactionCount, - }; + return errorsCount?.map((bucket) => { + const { key, doc_count: errorCount } = bucket; + return { x: key, y: errorCount / (transactionByTimestamp[key] || 1) }; }); } From f7d6e96c46f0cdd3035ba8bf0b00ea6e15ed19d1 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 8 Jun 2020 10:22:13 +0200 Subject: [PATCH 10/15] adding possibility to disable legend toggle --- .../components/app/ErrorGroupDetails/Distribution/index.tsx | 1 + .../apm/public/components/shared/charts/CustomPlot/Legends.js | 4 +++- .../public/components/shared/charts/ErrorRateChart/index.tsx | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index c8537fcdaec4c..bd0515f6d96f8 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -118,6 +118,7 @@ export function ErrorDistribution({ distribution, title }: Props) { color: theme.euiColorVis1, legendValue: average, title: 'Avg.', + legendClickDisabled: true, }, ]} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index bf6cf083e00ec..87ab81e738eb8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -105,7 +105,9 @@ export default function Legends({ return ( clickLegend(i)} + onClick={ + serie.legendClickDisabled ? undefined : () => clickLegend(i) + } disabled={seriesEnabledState[i]} text={text} color={serie.color} diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 6f0a4798d0204..8034826f81a60 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -72,6 +72,7 @@ export const ErrorRateChart = () => { color: theme.euiColorVis7, data: [], legendValue: average, + legendClickDisabled: true, title: 'Avg.', type: 'linemark', hideTooltipValue: true, From 519e8c5e4e5c92f436b2ed218e85ed787837bc6c Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 8 Jun 2020 15:37:02 +0200 Subject: [PATCH 11/15] removing x-axis ticks from histogram --- .../shared/charts/CustomPlot/StaticPlot.js | 12 ++------ .../shared/charts/CustomPlot/index.js | 10 +------ .../shared/charts/ErrorRateChart/index.tsx | 1 - .../__snapshots__/Histogram.test.js.snap | 28 +++++++++---------- .../shared/charts/Histogram/index.js | 3 +- 5 files changed, 18 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js index 0ae969e898cb7..d489970b55f29 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js @@ -158,13 +158,7 @@ class StaticPlot extends PureComponent { }; render() { - const { - series, - tickFormatY, - plotValues, - noHits, - xAxisTickSizeOuter = 0, - } = this.props; + const { series, tickFormatY, plotValues, noHits } = this.props; const { xTickValues, yTickValues } = plotValues; const tickFormatX = this.props.tickFormatX || this.tickFormatXTime; @@ -173,10 +167,9 @@ class StaticPlot extends PureComponent { {noHits ? ( {this.state.showAnnotations && !isEmpty(annotations) && ( @@ -234,7 +227,6 @@ InnerCustomPlot.propTypes = { width: PropTypes.number.isRequired, height: PropTypes.number, stackBy: PropTypes.string, - xAxisTickSizeOuter: PropTypes.number, annotations: PropTypes.arrayOf( PropTypes.shape({ type: PropTypes.string, diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 8034826f81a60..79c42d05463e4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -91,7 +91,6 @@ export const ErrorRateChart = () => { return asPercent(y, 1); }} height={unit * 10} - xAxisTickSizeOuter={10} /> ); diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index f1c7d4826fe0c..c0341cf45633c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -114,7 +114,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 0 ms @@ -149,7 +149,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 500 ms @@ -184,7 +184,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 1,000 ms @@ -219,7 +219,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 1,500 ms @@ -254,7 +254,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 2,000 ms @@ -289,7 +289,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 2,500 ms @@ -324,7 +324,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 3,000 ms diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index 8e914bff60faa..053829435212b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -156,8 +156,7 @@ export class HistogramInner extends PureComponent { Date: Tue, 9 Jun 2020 10:38:04 +0200 Subject: [PATCH 12/15] return no percent when transaction count doesn return hits --- .../apm/server/lib/errors/get_error_rate.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index 2301011022938..eb56dffd70bda 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -63,7 +63,10 @@ export async function getErrorRate({ aggs, }, }); - return resp.aggregations?.response_times.buckets; + return { + totalHits: resp.hits.total.value, + aggs: resp.aggregations?.response_times.buckets, + }; }; const getErrorBucketAggregation = async () => { const resp = await client.search({ @@ -86,20 +89,27 @@ export async function getErrorRate({ }; // Needed to the wrap the call to client.search in a function to avoid "Type instantiation is excessively deep and possibly infinite" - const [transactionsCount, errorsCount] = await Promise.all([ + const [transactions, errors] = await Promise.all([ getTransactionBucketAggregation(), getErrorBucketAggregation(), ]); const transactionByTimestamp: Record = {}; - if (transactionsCount) { - transactionsCount.forEach((bucket) => { + if (transactions?.aggs) { + transactions.aggs.forEach((bucket) => { transactionByTimestamp[bucket.key] = bucket.doc_count; }); } - return errorsCount?.map((bucket) => { + return errors?.map((bucket) => { const { key, doc_count: errorCount } = bucket; - return { x: key, y: errorCount / (transactionByTimestamp[key] || 1) }; + return { + x: key, + y: + // If the transaction count doesn't have any hits, it means that the there's no result + transactions?.totalHits === 0 + ? undefined + : errorCount / (transactionByTimestamp[key] || 1), + }; }); } From b14c2e790224da161e6bd7eaeaea85f2992c41fb Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Wed, 10 Jun 2020 11:56:29 +0200 Subject: [PATCH 13/15] addressing PR comments --- .../ErrorGroupDetails/Distribution/index.tsx | 11 +- .../shared/charts/ErrorRateChart/index.tsx | 8 +- .../shared/charts/Histogram/index.js | 235 ++++++++++-------- .../lib/errors/distribution/get_buckets.ts | 4 +- .../apm/server/lib/errors/get_error_rate.ts | 47 ++-- 5 files changed, 171 insertions(+), 134 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index bd0515f6d96f8..9ad65266d156c 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -20,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) @@ -33,7 +33,7 @@ interface IDistribution { interface FormattedBucket { x0: number; x: number; - y: number; + y: number | undefined; } export function getFormattedBuckets( @@ -69,10 +69,11 @@ export function ErrorDistribution({ distribution, title }: Props) { const average = useMemo(() => { const averageValue = buckets ? mean(buckets.map((bucket) => bucket.y)) : 0; + // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m return numeral(averageValue).format('0a'); }, [buckets]); - if (!buckets || distribution.noHits) { + if (!buckets) { return ( { data: [], legendValue: average, legendClickDisabled: true, - title: 'Avg.', + title: i18n.translate('xpack.apm.errorRateCharrt.avgLabel', { + defaultMessage: 'Avg.', + }), type: 'linemark', hideTooltipValue: true, }, @@ -82,7 +84,9 @@ export const ErrorRateChart = () => { type: 'line', color: theme.euiColorVis7, hideLegend: true, - title: 'Rate', + title: i18n.translate('xpack.apm.errorRateCharrt.rateLabel', { + defaultMessage: 'Rate', + }), }, ]} onHover={combinedOnHover} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index 053829435212b..aeb70e29142d1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -27,6 +27,9 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { tint } from 'polished'; import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone'; import Legends from '../CustomPlot/Legends'; +import StatusText from '../CustomPlot/StatusText'; +import { i18n } from '@kbn/i18n'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; const XY_HEIGHT = unit * 10; const XY_MARGIN = { @@ -141,111 +144,139 @@ export class HistogramInner extends PureComponent { const showVerticalLineHover = verticalLineHover(hoveredBucket); const showBackgroundHover = backgroundHover(hoveredBucket); + const hasValidCoordinates = buckets.some((bucket) => + isValidCoordinateValue(bucket.y) + ); + const noHits = !hasValidCoordinates; + + const xyPlotProps = { + dontCheckIfEmpty: true, + xType: this.props.xType, + width: XY_WIDTH, + height: XY_HEIGHT, + margin: XY_MARGIN, + xDomain: xDomain, + yDomain: yDomain, + }; + + const xAxisProps = { + style: { strokeWidth: '1px' }, + marginRight: 10, + tickSize: 0, + tickTotal: X_TICK_TOTAL, + tickFormat: formatX, + tickValues: xTickValues, + }; + + const emptyStateChart = ( + + + + + ); + return (
- - - - - - {showBackgroundHover && ( - - )} - - {shouldShowTooltip && ( - - )} - - {selectedBucket && ( - - )} - - - - {showVerticalLineHover && ( - - )} - - { - return { - ...bucket, - xCenter: (bucket.x0 + bucket.x) / 2, - }; - })} - onClick={this.onClick} - onHover={this.onHover} - onBlur={this.onBlur} - x={(d) => x(d.xCenter)} - y={() => 1} - /> - - - {legends && ( - {}} - truncateLegends={false} - noHits={false} - /> + {noHits ? ( + <>{emptyStateChart} + ) : ( + <> + + + + + + {showBackgroundHover && ( + + )} + + {shouldShowTooltip && ( + + )} + + {selectedBucket && ( + + )} + + + + {showVerticalLineHover && ( + + )} + + { + return { + ...bucket, + xCenter: (bucket.x0 + bucket.x) / 2, + }; + })} + onClick={this.onClick} + onHover={this.onHover} + onBlur={this.onBlur} + x={(d) => x(d.xCenter)} + y={() => 1} + /> + + + {legends && ( + {}} + truncateLegends={false} + noHits={false} + /> + )} + )}
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 7d96c490fcd70..29fc872f1917c 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -67,10 +67,12 @@ export async function getBuckets({ const resp = await client.search(params); + const totalHits = resp.hits.total.value; + const buckets = (resp.aggregations?.distribution.buckets || []).map( (bucket) => ({ key: bucket.key, - count: bucket.doc_count, + count: totalHits === 0 ? undefined : bucket.doc_count, }) ); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index eb56dffd70bda..4f498fa0b52ea 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../common/processor_event'; import { ERROR_GROUP_ID, - SERVICE_NAME, PROCESSOR_EVENT, + SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getBucketSize } from '../helpers/get_bucket_size'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { rangeFilter } from '../helpers/range_filter'; import { Setup, @@ -27,8 +27,6 @@ export async function getErrorRate({ setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, client, indices } = setup; - const { intervalString } = getBucketSize(start, end, 'auto'); - const groupIdTerm = groupId ? [{ term: { [ERROR_GROUP_ID]: groupId } }] : []; const filter = [ { term: { [SERVICE_NAME]: serviceName } }, @@ -38,12 +36,7 @@ export async function getErrorRate({ const aggs = { response_times: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }, + date_histogram: getMetricsDateHistogramParams(start, end), }, }; @@ -65,10 +58,13 @@ export async function getErrorRate({ }); return { totalHits: resp.hits.total.value, - aggs: resp.aggregations?.response_times.buckets, + responseTimeBuckets: resp.aggregations?.response_times.buckets, }; }; const getErrorBucketAggregation = async () => { + const groupIdFilter = groupId + ? [{ term: { [ERROR_GROUP_ID]: groupId } }] + : []; const resp = await client.search({ index: indices['apm_oss.errorIndices'], body: { @@ -77,7 +73,7 @@ export async function getErrorRate({ bool: { filter: [ ...filter, - ...groupIdTerm, + ...groupIdFilter, { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, ], }, @@ -88,28 +84,29 @@ export async function getErrorRate({ return resp.aggregations?.response_times.buckets; }; - // Needed to the wrap the call to client.search in a function to avoid "Type instantiation is excessively deep and possibly infinite" - const [transactions, errors] = await Promise.all([ + const [transactions, errorResponseTimeBuckets] = await Promise.all([ getTransactionBucketAggregation(), getErrorBucketAggregation(), ]); - const transactionByTimestamp: Record = {}; - if (transactions?.aggs) { - transactions.aggs.forEach((bucket) => { - transactionByTimestamp[bucket.key] = bucket.doc_count; + const transactionCountByTimestamp: Record = {}; + if (transactions?.responseTimeBuckets) { + transactions.responseTimeBuckets.forEach((bucket) => { + transactionCountByTimestamp[bucket.key] = bucket.doc_count; }); } - return errors?.map((bucket) => { + const errorRates = errorResponseTimeBuckets?.map((bucket) => { const { key, doc_count: errorCount } = bucket; + const transactionCount = transactionCountByTimestamp[key] || 1; + const relativeRate = errorCount / transactionCount; + return { x: key, - y: - // If the transaction count doesn't have any hits, it means that the there's no result - transactions?.totalHits === 0 - ? undefined - : errorCount / (transactionByTimestamp[key] || 1), + // If the transaction count doesn't have any hits, it means that there's no result + y: transactions?.totalHits === 0 ? undefined : relativeRate, }; }); + + return errorRates || []; } From fd78169f4fdb4f4ec32f0b0a47a14399d717b182 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 15 Jun 2020 10:59:52 +0200 Subject: [PATCH 14/15] addressing PR comments --- .../ErrorGroupDetails/Distribution/index.tsx | 17 ++++++-------- .../shared/charts/CustomPlot/index.js | 4 +++- .../shared/charts/ErrorRateChart/index.tsx | 23 ++++++++----------- .../shared/charts/Histogram/index.js | 8 ++++--- .../lib/errors/distribution/get_buckets.ts | 4 +--- .../apm/server/lib/errors/get_error_rate.ts | 12 ++++------ 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 9ad65266d156c..d71d5f2cb480d 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -5,13 +5,13 @@ */ import { EuiTitle } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; +import d3 from 'd3'; import { scaleUtc } from 'd3-scale'; import mean from 'lodash.mean'; -import d3 from 'd3'; -import React, { useMemo } from 'react'; +import React from 'react'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; // @ts-ignore @@ -67,12 +67,6 @@ export function ErrorDistribution({ distribution, title }: Props) { distribution.bucketSize ); - const average = useMemo(() => { - const averageValue = buckets ? mean(buckets.map((bucket) => bucket.y)) : 0; - // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m - return numeral(averageValue).format('0a'); - }, [buckets]); - if (!buckets) { return ( 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(); @@ -93,6 +88,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time-utc" @@ -117,7 +113,8 @@ export function ErrorDistribution({ distribution, title }: Props) { legends={[ { color: theme.euiColorVis1, - legendValue: average, + // 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.', }), diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index 3d7ae5ae1cac8..7e74961e57ea1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -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, @@ -234,6 +234,7 @@ InnerCustomPlot.propTypes = { firstSeen: PropTypes.number, }) ), + noHits: PropTypes.bool, }; InnerCustomPlot.defaultProps = { @@ -242,6 +243,7 @@ InnerCustomPlot.defaultProps = { tickFormatY: (y) => y, truncateLegends: false, xAxisTickSizeOuter: 0, + noHits: false, }; export default makeWidthFlexible(InnerCustomPlot); diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index af7e9aba7a06f..585379c469798 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -7,7 +7,7 @@ import { EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import mean from 'lodash.mean'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; @@ -26,7 +26,7 @@ export const ErrorRateChart = () => { const syncedChartsProps = useChartsSync(); const { serviceName, start, end, errorGroupId } = urlParams; - const { data: errorRateData = [] } = useFetcher(() => { + const { data: errorRateData } = useFetcher(() => { if (serviceName && start && end) { return callApmApi({ pathname: '/api/apm/services/{serviceName}/errors/rate', @@ -45,10 +45,6 @@ export const ErrorRateChart = () => { } }, [serviceName, start, end, uiFilters, errorGroupId]); - const average = useMemo(() => { - return tickFormatY(mean(errorRateData.map((rate) => rate.y))); - }, [errorRateData]); - const combinedOnHover = useCallback( (hoverX: number) => { return syncedChartsProps.onHover(hoverX); @@ -56,6 +52,8 @@ export const ErrorRateChart = () => { [syncedChartsProps] ); + const errorRates = errorRateData?.errorRates || []; + return ( <> @@ -67,33 +65,32 @@ export const ErrorRateChart = () => { rate.y))), legendClickDisabled: true, - title: i18n.translate('xpack.apm.errorRateCharrt.avgLabel', { + title: i18n.translate('xpack.apm.errorRateChart.avgLabel', { defaultMessage: 'Avg.', }), type: 'linemark', hideTooltipValue: true, }, { - data: errorRateData, + data: errorRates, type: 'line', color: theme.euiColorVis7, hideLegend: true, - title: i18n.translate('xpack.apm.errorRateCharrt.rateLabel', { + title: i18n.translate('xpack.apm.errorRateChart.rateLabel', { defaultMessage: 'Rate', }), }, ]} onHover={combinedOnHover} tickFormatY={tickFormatY} - formatTooltipValue={({ y }: { y: number }) => { - return asPercent(y, 1); - }} + formatTooltipValue={({ y }: { y: number }) => tickFormatY(y)} height={unit * 10} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index aeb70e29142d1..002ff19d0d1df 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -147,7 +147,7 @@ export class HistogramInner extends PureComponent { const hasValidCoordinates = buckets.some((bucket) => isValidCoordinateValue(bucket.y) ); - const noHits = !hasValidCoordinates; + const noHits = this.props.noHits || !hasValidCoordinates; const xyPlotProps = { dontCheckIfEmpty: true, @@ -243,7 +243,7 @@ export class HistogramInner extends PureComponent { }} /> - {showVerticalLineHover && ( + {showVerticalLineHover && hoveredBucket?.x && ( )} @@ -273,7 +273,7 @@ export class HistogramInner extends PureComponent { hiddenSeriesCount={0} clickLegend={() => {}} truncateLegends={false} - noHits={false} + noHits={noHits} /> )} @@ -299,6 +299,7 @@ HistogramInner.propTypes = { width: PropTypes.number.isRequired, xType: PropTypes.string, legends: PropTypes.array, + noHits: PropTypes.bool, }; HistogramInner.defaultProps = { @@ -309,6 +310,7 @@ HistogramInner.defaultProps = { tooltipHeader: () => null, verticalLineHover: () => null, xType: 'linear', + noHits: false, }; export default makeWidthFlexible(HistogramInner); diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 29fc872f1917c..7d96c490fcd70 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -67,12 +67,10 @@ export async function getBuckets({ const resp = await client.search(params); - const totalHits = resp.hits.total.value; - const buckets = (resp.aggregations?.distribution.buckets || []).map( (bucket) => ({ key: bucket.key, - count: totalHits === 0 ? undefined : bucket.doc_count, + count: bucket.doc_count, }) ); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index 4f498fa0b52ea..d7156434b2556 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -100,13 +100,11 @@ export async function getErrorRate({ const { key, doc_count: errorCount } = bucket; const transactionCount = transactionCountByTimestamp[key] || 1; const relativeRate = errorCount / transactionCount; - - return { - x: key, - // If the transaction count doesn't have any hits, it means that there's no result - y: transactions?.totalHits === 0 ? undefined : relativeRate, - }; + return { x: key, y: relativeRate }; }); - return errorRates || []; + return { + noHits: transactions?.totalHits === 0, + errorRates, + }; } From fa343da01e21f97b27b34408f3f8fe2839ec591a Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 18 Jun 2020 10:01:55 +0200 Subject: [PATCH 15/15] returning null when there is no transaction count --- .../public/components/shared/charts/ErrorRateChart/index.tsx | 4 +++- x-pack/plugins/apm/server/lib/errors/get_error_rate.ts | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx index 585379c469798..7aafa9e1fdcec 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -90,7 +90,9 @@ export const ErrorRateChart = () => { ]} onHover={combinedOnHover} tickFormatY={tickFormatY} - formatTooltipValue={({ y }: { y: number }) => tickFormatY(y)} + formatTooltipValue={({ y }: { y?: number }) => + Number.isFinite(y) ? tickFormatY(y) : 'N/A' + } height={unit * 10} /> diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts index d7156434b2556..d558e3942a42b 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -98,8 +98,7 @@ export async function getErrorRate({ const errorRates = errorResponseTimeBuckets?.map((bucket) => { const { key, doc_count: errorCount } = bucket; - const transactionCount = transactionCountByTimestamp[key] || 1; - const relativeRate = errorCount / transactionCount; + const relativeRate = errorCount / transactionCountByTimestamp[key]; return { x: key, y: relativeRate }; });