From f69edbd89bb5a3b3b4f4325156c9a4174f4787d7 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 15 Jul 2020 07:17:54 -0500 Subject: [PATCH] [APM] Add error rates to Service Map popovers (#69520) Make the `getErrorRate` function used in the error rate charts additionally take `service.environment` as a filter and have it return the `average` of the values. Call that function in the API for the service map metrics. Fixes #68160. Co-authored-by: cauemarcondes --- x-pack/plugins/apm/common/service_map.ts | 4 +- .../app/ServiceMap/Popover/Contents.tsx | 4 +- .../app/ServiceMap/Popover/Info.tsx | 4 +- .../ServiceMap/Popover/Popover.stories.tsx | 156 +++++-- ...ricFetcher.tsx => ServiceStatsFetcher.tsx} | 31 +- ...iceMetricList.tsx => ServiceStatsList.tsx} | 36 +- .../get_parsed_ui_filters.ts | 23 + .../get_service_map_service_node_info.test.ts | 81 ++++ .../get_service_map_service_node_info.ts | 100 ++-- .../lib/transaction_groups/get_error_rate.ts | 11 +- .../plugins/apm/server/routes/service_map.ts | 17 +- .../apm/server/routes/transaction_groups.ts | 10 +- .../translations/translations/ja-JP.json | 7 - .../translations/translations/zh-CN.json | 7 - .../trial/tests/service_maps.ts | 428 ++++++++++-------- 15 files changed, 568 insertions(+), 351 deletions(-) rename x-pack/plugins/apm/public/components/app/ServiceMap/Popover/{ServiceMetricFetcher.tsx => ServiceStatsFetcher.tsx} (78%) rename x-pack/plugins/apm/public/components/app/ServiceMap/Popover/{ServiceMetricList.tsx => ServiceStatsList.tsx} (75%) create mode 100644 x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts create mode 100644 x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index b50db270ef54..7f46fc685d9c 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -36,14 +36,14 @@ export interface Connection { destination: ConnectionNode; } -export interface ServiceNodeMetrics { +export interface ServiceNodeStats { avgMemoryUsage: number | null; avgCpuUsage: number | null; transactionStats: { avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }; - avgErrorsPerMinute: number | null; + avgErrorRate: number | null; } export function isValidPlatinumLicense(license: ILicense) { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index c696a93773ce..78466b2659bb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -14,7 +14,7 @@ import cytoscape from 'cytoscape'; import React, { MouseEvent } from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; -import { ServiceMetricFetcher } from './ServiceMetricFetcher'; +import { ServiceStatsFetcher } from './ServiceStatsFetcher'; import { popoverWidth } from '../cytoscapeOptions'; interface ContentsProps { @@ -70,7 +70,7 @@ export function Contents({ {isService ? ( - diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 223d342e6799..094cf032c4c9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -38,13 +38,13 @@ export function Info(data: InfoProps) { const listItems = [ { - title: i18n.translate('xpack.apm.serviceMap.typePopoverMetric', { + title: i18n.translate('xpack.apm.serviceMap.typePopoverStat', { defaultMessage: 'Type', }), description: type, }, { - title: i18n.translate('xpack.apm.serviceMap.subtypePopoverMetric', { + title: i18n.translate('xpack.apm.serviceMap.subtypePopoverStat', { defaultMessage: 'Subtype', }), description: subtype, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index ccf147ed1d90..20f6f92f9995 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -5,40 +5,128 @@ */ import { storiesOf } from '@storybook/react'; +import cytoscape from 'cytoscape'; +import { HttpSetup } from 'kibana/public'; import React from 'react'; -import { ServiceMetricList } from './ServiceMetricList'; +import { EuiThemeProvider } from '../../../../../../observability/public'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockUrlParamsContextProvider } from '../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; +import { CytoscapeContext } from '../Cytoscape'; +import { Popover } from './'; +import { ServiceStatsList } from './ServiceStatsList'; -storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) - .add('example', () => ( - { + const node = { + data: { id: 'example service', 'service.name': 'example service' }, + }; + const cy = cytoscape({ elements: [node] }); + const httpMock = ({ + get: async () => ({ + avgCpuUsage: 0.32809666568309237, + avgErrorRate: 0.556068173242986, + avgMemoryUsage: 0.5504868173242986, avgRequestsPerMinute: 164.47222031860858, - }} - avgCpuUsage={0.32809666568309237} - avgMemoryUsage={0.5504868173242986} - /> - )) - .add('some null values', () => ( - - )) - .add('all null values', () => ( - - )); + avgTransactionDuration: 61634.38905590272, + }), + } as unknown) as HttpSetup; + + createCallApmApi(httpMock); + + setImmediate(() => { + cy.$('example service').select(); + }); + + return ( + + + + +
{storyFn()}
+
+
+
+
+ ); + }) + .add( + 'example', + () => { + return ; + }, + { + info: { + propTablesExclude: [ + CytoscapeContext.Provider, + MockApmPluginContextWrapper, + MockUrlParamsContextProvider, + EuiThemeProvider, + ], + source: false, + }, + } + ); + +storiesOf('app/ServiceMap/Popover/ServiceStatsList', module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'example', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ) + .add( + 'loading', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ) + .add( + 'some null values', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ) + .add( + 'all null values', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx similarity index 78% rename from x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index 957678877a13..9e8f1f7a0171 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -13,39 +13,44 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; -import { ServiceNodeMetrics } from '../../../../../common/service_map'; +import { ServiceNodeStats } from '../../../../../common/service_map'; +import { ServiceStatsList } from './ServiceStatsList'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { ServiceMetricList } from './ServiceMetricList'; import { AnomalyDetection } from './AnomalyDetection'; import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; -interface ServiceMetricFetcherProps { +interface ServiceStatsFetcherProps { + environment?: string; serviceName: string; serviceAnomalyStats: ServiceAnomalyStats | undefined; } -export function ServiceMetricFetcher({ +export function ServiceStatsFetcher({ serviceName, serviceAnomalyStats, -}: ServiceMetricFetcherProps) { +}: ServiceStatsFetcherProps) { const { - urlParams: { start, end, environment }, + urlParams: { start, end }, + uiFilters, } = useUrlParams(); const { - data = { transactionStats: {} } as ServiceNodeMetrics, + data = { transactionStats: {} } as ServiceNodeStats, status, } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ pathname: '/api/apm/service-map/service/{serviceName}', - params: { path: { serviceName }, query: { start, end, environment } }, + params: { + path: { serviceName }, + query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + }, }); } }, - [serviceName, start, end, environment], + [serviceName, start, end, uiFilters], { preservePreviousData: false, } @@ -60,20 +65,20 @@ export function ServiceMetricFetcher({ const { avgCpuUsage, - avgErrorsPerMinute, + avgErrorRate, avgMemoryUsage, transactionStats: { avgRequestsPerMinute, avgTransactionDuration }, } = data; const hasServiceData = [ avgCpuUsage, - avgErrorsPerMinute, + avgErrorRate, avgMemoryUsage, avgRequestsPerMinute, avgTransactionDuration, ].some((stat) => isNumber(stat)); - if (environment && !hasServiceData) { + if (!hasServiceData) { return ( {i18n.translate('xpack.apm.serviceMap.popoverMetrics.noDataText', { @@ -93,7 +98,7 @@ export function ServiceMetricFetcher({ )} - + ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index f82f434e7ded..4a1a291249f5 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; -import { ServiceNodeMetrics } from '../../../../../common/service_map'; +import { ServiceNodeStats } from '../../../../../common/service_map'; import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; export const ItemRow = styled('tr')` @@ -24,18 +24,18 @@ export const ItemDescription = styled('td')` text-align: right; `; -type ServiceMetricListProps = ServiceNodeMetrics; +type ServiceStatsListProps = ServiceNodeStats; -export function ServiceMetricList({ - avgErrorsPerMinute, +export function ServiceStatsList({ + transactionStats, + avgErrorRate, avgCpuUsage, avgMemoryUsage, - transactionStats, -}: ServiceMetricListProps) { +}: ServiceStatsListProps) { const listItems = [ { title: i18n.translate( - 'xpack.apm.serviceMap.avgTransDurationPopoverMetric', + 'xpack.apm.serviceMap.avgTransDurationPopoverStat', { defaultMessage: 'Trans. duration (avg.)', } @@ -58,27 +58,21 @@ export function ServiceMetricList({ : null, }, { - title: i18n.translate( - 'xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric', - { - defaultMessage: 'Errors per minute (avg.)', - } - ), - description: avgErrorsPerMinute?.toFixed(2), + title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { + defaultMessage: 'Error rate (avg.)', + }), + description: isNumber(avgErrorRate) ? asPercent(avgErrorRate, 1) : null, }, { - title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverMetric', { + title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', { defaultMessage: 'CPU usage (avg.)', }), description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : null, }, { - title: i18n.translate( - 'xpack.apm.serviceMap.avgMemoryUsagePopoverMetric', - { - defaultMessage: 'Memory usage (avg.)', - } - ), + title: i18n.translate('xpack.apm.serviceMap.avgMemoryUsagePopoverStat', { + defaultMessage: 'Memory usage (avg.)', + }), description: isNumber(avgMemoryUsage) ? asPercent(avgMemoryUsage, 1) : null, diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts new file mode 100644 index 000000000000..324da199807c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from 'src/core/server'; +import { UIFilters } from '../../../../typings/ui_filters'; + +export function getParsedUiFilters({ + uiFilters, + logger, +}: { + uiFilters: string; + logger: Logger; +}): UIFilters { + try { + return JSON.parse(uiFilters); + } catch (error) { + logger.error(error); + } + return {}; +} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts new file mode 100644 index 000000000000..1e0d001340ed --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { getServiceMapServiceNodeInfo } from './get_service_map_service_node_info'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import * as getErrorRateModule from '../transaction_groups/get_error_rate'; + +describe('getServiceMapServiceNodeInfo', () => { + describe('with no results', () => { + it('returns null data', async () => { + const setup = ({ + client: { + search: () => + Promise.resolve({ + hits: { total: { value: 0 } }, + }), + }, + indices: {}, + } as unknown) as Setup & SetupTimeRange; + const environment = 'test environment'; + const serviceName = 'test service name'; + const result = await getServiceMapServiceNodeInfo({ + uiFilters: { environment }, + setup, + serviceName, + }); + + expect(result).toEqual({ + avgCpuUsage: null, + avgErrorRate: null, + avgMemoryUsage: null, + transactionStats: { + avgRequestsPerMinute: null, + avgTransactionDuration: null, + }, + }); + }); + }); + + describe('with some results', () => { + it('returns data', async () => { + jest.spyOn(getErrorRateModule, 'getErrorRate').mockResolvedValueOnce({ + average: 0.5, + erroneousTransactionsRate: [], + noHits: false, + }); + + const setup = ({ + client: { + search: () => + Promise.resolve({ + hits: { total: { value: 1 } }, + }), + }, + indices: {}, + start: 1593460053026000, + end: 1593497863217000, + } as unknown) as Setup & SetupTimeRange; + const environment = 'test environment'; + const serviceName = 'test service name'; + const result = await getServiceMapServiceNodeInfo({ + uiFilters: { environment }, + setup, + serviceName, + }); + + expect(result).toEqual({ + avgCpuUsage: null, + avgErrorRate: 0.5, + avgMemoryUsage: null, + transactionStats: { + avgRequestsPerMinute: 0.000001586873761097901, + avgTransactionDuration: null, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index dd5d19b620c5..0f7136d6d74a 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -4,23 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { ESFilter } from '../../../typings/elasticsearch'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { UIFilters } from '../../../typings/ui_filters'; import { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { ESFilter } from '../../../typings/elasticsearch'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; import { TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD, } from '../../../common/transaction_types'; +import { getErrorRate } from '../transaction_groups/get_error_rate'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; interface Options { @@ -30,69 +33,72 @@ interface Options { } interface TaskParameters { - setup: Setup; - minutes: number; + environment?: string; filter: ESFilter[]; + minutes: number; + serviceName?: string; + setup: Setup; } export async function getServiceMapServiceNodeInfo({ serviceName, - environment, setup, -}: Options & { serviceName: string; environment?: string }) { + uiFilters, +}: Options & { serviceName: string; uiFilters: UIFilters }) { const { start, end } = setup; const filter: ESFilter[] = [ { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, - ...getEnvironmentUiFilterES(environment), + ...getEnvironmentUiFilterES(uiFilters.environment), ]; const minutes = Math.abs((end - start) / (1000 * 60)); - const taskParams = { setup, minutes, filter }; + const taskParams = { + environment: uiFilters.environment, + filter, + minutes, + serviceName, + setup, + }; const [ - errorMetrics, + errorStats, transactionStats, - cpuMetrics, - memoryMetrics, + cpuStats, + memoryStats, ] = await Promise.all([ - getErrorMetrics(taskParams), + getErrorStats(taskParams), getTransactionStats(taskParams), - getCpuMetrics(taskParams), - getMemoryMetrics(taskParams), + getCpuStats(taskParams), + getMemoryStats(taskParams), ]); - return { - ...errorMetrics, + ...errorStats, transactionStats, - ...cpuMetrics, - ...memoryMetrics, + ...cpuStats, + ...memoryStats, }; } -async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) { - const { client, indices } = setup; - - const response = await client.search({ - index: indices['apm_oss.errorIndices'], - body: { - size: 0, - query: { - bool: { - filter: filter.concat({ term: { [PROCESSOR_EVENT]: 'error' } }), - }, - }, - track_total_hits: true, - }, - }); - - return { - avgErrorsPerMinute: - response.hits.total.value > 0 - ? response.hits.total.value / minutes - : null, +async function getErrorStats({ + setup, + serviceName, + environment, +}: { + setup: Options['setup']; + serviceName: string; + environment?: string; +}) { + const setupWithBlankUiFilters = { + ...setup, + uiFiltersES: getEnvironmentUiFilterES(environment), }; + const { noHits, average } = await getErrorRate({ + setup: setupWithBlankUiFilters, + serviceName, + }); + return { avgErrorRate: noHits ? null : average }; } async function getTransactionStats({ @@ -113,7 +119,7 @@ async function getTransactionStats({ bool: { filter: [ ...filter, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { terms: { [TRANSACTION_TYPE]: [ @@ -137,7 +143,7 @@ async function getTransactionStats({ }; } -async function getCpuMetrics({ +async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { @@ -150,7 +156,7 @@ async function getCpuMetrics({ query: { bool: { filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: 'metric' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }, ]), }, @@ -162,7 +168,7 @@ async function getCpuMetrics({ return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null }; } -async function getMemoryMetrics({ +async function getMemoryStats({ setup, filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 5b66f7d7a45e..6a1ee8daad7c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -3,11 +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 { mean } from 'lodash'; import { PROCESSOR_EVENT, HTTP_RESPONSE_STATUS_CODE, TRANSACTION_NAME, TRANSACTION_TYPE, + SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -39,6 +41,7 @@ export async function getErrorRate({ : []; const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: rangeFilter(start, end) }, { exists: { field: HTTP_RESPONSE_STATUS_CODE } }, @@ -82,5 +85,11 @@ export async function getErrorRate({ } ) || []; - return { noHits, erroneousTransactionsRate }; + const average = mean( + erroneousTransactionsRate + .map((errorRate) => errorRate.y) + .filter((y) => isFinite(y)) + ); + + return { noHits, erroneousTransactionsRate, average }; } diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 50123131a42e..971e247d9898 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -14,8 +14,9 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; -import { rangeRt } from './default_api_types'; +import { rangeRt, uiFiltersRt } from './default_api_types'; import { APM_SERVICE_MAPS_FEATURE_NAME } from '../feature'; +import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const serviceMapRoute = createRoute(() => ({ path: '/api/apm/service-map', @@ -52,12 +53,7 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ path: t.type({ serviceName: t.string, }), - query: t.intersection([ - rangeRt, - t.partial({ - environment: t.string, - }), - ]), + query: t.intersection([rangeRt, uiFiltersRt]), }, handler: async ({ context, request }) => { if (!context.config['xpack.apm.serviceMapEnabled']) { @@ -66,17 +62,20 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ if (!isValidPlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } + const logger = context.logger; const setup = await setupRequest(context, request); const { - query: { environment }, + query: { uiFilters: uiFiltersJson }, path: { serviceName }, } = context.params; + const uiFilters = getParsedUiFilters({ uiFilters: uiFiltersJson, logger }); + return getServiceMapServiceNodeInfo({ setup, serviceName, - environment, + uiFilters, }); }, })); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index dca2fb1d9b29..813d757c7c33 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -15,7 +15,7 @@ import { uiFiltersRt, rangeRt } from './default_api_types'; import { getTransactionAvgDurationByBrowser } from '../lib/transactions/avg_duration_by_browser'; import { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country'; import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; -import { UIFilters } from '../../typings/ui_filters'; +import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const transactionGroupsRoute = createRoute(() => ({ path: '/api/apm/services/{serviceName}/transaction_groups', @@ -71,12 +71,8 @@ export const transactionGroupsChartsRoute = createRoute(() => ({ transactionName, uiFilters: uiFiltersJson, } = context.params.query; - let uiFilters: UIFilters = {}; - try { - uiFilters = JSON.parse(uiFiltersJson); - } catch (error) { - logger.error(error); - } + + const uiFilters = getParsedUiFilters({ uiFilters: uiFiltersJson, logger }); return getTransactionCharts({ serviceName, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b54f88f83fbe..a4100ae914b2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4286,11 +4286,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "メトリック", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "トランザクション", - "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU使用状況 (平均)", - "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "1分あたりのエラー(平均)", - "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "メモリー使用状況(平均)", - "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "1分あたりのリクエスト(平均)", - "xpack.apm.serviceMap.avgTransDurationPopoverMetric": "トランザクションの長さ(平均)", "xpack.apm.serviceMap.betaBadge": "ベータ", "xpack.apm.serviceMap.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.serviceMap.center": "中央", @@ -4300,8 +4295,6 @@ "xpack.apm.serviceMap.focusMapButtonText": "焦点マップ", "xpack.apm.serviceMap.invalidLicenseMessage": "サービスマップを利用するには、Elastic Platinum ライセンスが必要です。これにより、APM データとともにアプリケーションスタック全てを可視化することができるようになります。", "xpack.apm.serviceMap.serviceDetailsButtonText": "サービス詳細", - "xpack.apm.serviceMap.subtypePopoverMetric": "サブタイプ", - "xpack.apm.serviceMap.typePopoverMetric": "タイプ", "xpack.apm.serviceMap.viewFullMap": "サービスの全体マップを表示", "xpack.apm.serviceMap.zoomIn": "ズームイン", "xpack.apm.serviceMap.zoomOut": "ズームアウト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 389e0083d5a9..69e37f3f9f9f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4290,11 +4290,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "指标", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "事务", - "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU 使用(平均)", - "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "每分钟错误数(平均)", - "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "内存使用(平均)", - "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "每分钟请求数(平均)", - "xpack.apm.serviceMap.avgTransDurationPopoverMetric": "事务持续时间(平均)", "xpack.apm.serviceMap.betaBadge": "公测版", "xpack.apm.serviceMap.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。", "xpack.apm.serviceMap.center": "中", @@ -4304,8 +4299,6 @@ "xpack.apm.serviceMap.focusMapButtonText": "聚焦地图", "xpack.apm.serviceMap.invalidLicenseMessage": "要访问服务地图,必须订阅 Elastic 白金级许可证。使用该许可证,您将能够可视化整个应用程序堆栈以及 APM 数据。", "xpack.apm.serviceMap.serviceDetailsButtonText": "服务详情", - "xpack.apm.serviceMap.subtypePopoverMetric": "子类型", - "xpack.apm.serviceMap.typePopoverMetric": "类型", "xpack.apm.serviceMap.viewFullMap": "查看完整的服务地图", "xpack.apm.serviceMap.zoomIn": "放大", "xpack.apm.serviceMap.zoomOut": "缩小", diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps.ts index cf265c3fb673..0b370f6a30a8 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import querystring from 'querystring'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -11,159 +12,224 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + describe('Service Maps with a trial license', () => { + describe('/api/apm/service-map', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); - describe('Service Maps', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ elements: [] }); + expect(response.status).to.be(200); + expect(response.body).to.eql({ elements: [] }); + }); }); - }); - describe('when there is data', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + describe('when there is data', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); - it('returns service map elements', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + it('returns service map elements', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); - expect(response.status).to.be(200); - expect(response.body).to.eql({ - elements: [ - { - data: { - source: 'client', - target: 'opbeans-node', - id: 'client~opbeans-node', - sourceData: { - id: 'client', - 'service.name': 'client', - 'agent.name': 'rum-js', + expect(response.status).to.be(200); + + expect(response.body).to.eql({ + elements: [ + { + data: { + source: 'client', + target: 'opbeans-node', + id: 'client~opbeans-node', + sourceData: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, }, - targetData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', + }, + { + data: { + source: 'opbeans-java', + target: '>opbeans-java:3000', + id: 'opbeans-java~>opbeans-java:3000', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': 'opbeans-java:3000', + 'span.type': 'external', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', + }, }, }, - }, - { - data: { - source: 'opbeans-java', - target: '>opbeans-java:3000', - id: 'opbeans-java~>opbeans-java:3000', - sourceData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + { + data: { + source: 'opbeans-java', + target: '>postgresql', + id: 'opbeans-java~>postgresql', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, }, - targetData: { - 'span.subtype': 'http', - 'span.destination.service.resource': 'opbeans-java:3000', - 'span.type': 'external', - id: '>opbeans-java:3000', - label: 'opbeans-java:3000', + }, + { + data: { + source: 'opbeans-java', + target: 'opbeans-node', + id: 'opbeans-java~opbeans-node', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + bidirectional: true, }, }, - }, - { - data: { - source: 'opbeans-java', - target: '>postgresql', - id: 'opbeans-java~>postgresql', - sourceData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + { + data: { + source: 'opbeans-node', + target: '>93.184.216.34:80', + id: 'opbeans-node~>93.184.216.34:80', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, }, - targetData: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', + }, + { + data: { + source: 'opbeans-node', + target: '>postgresql', + id: 'opbeans-node~>postgresql', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, }, }, - }, - { - data: { - source: 'opbeans-java', - target: 'opbeans-node', - id: 'opbeans-java~opbeans-node', - sourceData: { + { + data: { + source: 'opbeans-node', + target: '>redis', + id: 'opbeans-node~>redis', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'redis', + 'span.destination.service.resource': 'redis', + 'span.type': 'cache', + id: '>redis', + label: 'redis', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: 'opbeans-java', + id: 'opbeans-node~opbeans-java', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + isInverseEdge: true, + }, + }, + { + data: { id: 'opbeans-java', 'service.environment': 'production', 'service.name': 'opbeans-java', 'agent.name': 'java', }, - targetData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - bidirectional: true, }, - }, - { - data: { - source: 'opbeans-node', - target: '>93.184.216.34:80', - id: 'opbeans-node~>93.184.216.34:80', - sourceData: { + { + data: { id: 'opbeans-node', 'service.environment': 'production', 'service.name': 'opbeans-node', 'agent.name': 'nodejs', }, - targetData: { + }, + { + data: { 'span.subtype': 'http', - 'span.destination.service.resource': '93.184.216.34:80', + 'span.destination.service.resource': 'opbeans-java:3000', 'span.type': 'external', - id: '>93.184.216.34:80', - label: '93.184.216.34:80', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', }, }, - }, - { - data: { - source: 'opbeans-node', - target: '>postgresql', - id: 'opbeans-node~>postgresql', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', + { + data: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', }, }, - }, - { - data: { - source: 'opbeans-node', - target: '>redis', - id: 'opbeans-node~>redis', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { + { + data: { 'span.subtype': 'redis', 'span.destination.service.resource': 'redis', 'span.type': 'cache', @@ -171,87 +237,51 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) label: 'redis', }, }, - }, - { - data: { - source: 'opbeans-node', - target: 'opbeans-java', - id: 'opbeans-node~opbeans-java', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + { + data: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', }, - isInverseEdge: true, - }, - }, - { - data: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', - }, - }, - { - data: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - }, - { - data: { - 'span.subtype': 'http', - 'span.destination.service.resource': 'opbeans-java:3000', - 'span.type': 'external', - id: '>opbeans-java:3000', - label: 'opbeans-java:3000', - }, - }, - { - data: { - id: 'client', - 'service.name': 'client', - 'agent.name': 'rum-js', - }, - }, - { - data: { - 'span.subtype': 'redis', - 'span.destination.service.resource': 'redis', - 'span.type': 'cache', - id: '>redis', - label: 'redis', - }, - }, - { - data: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', }, - }, - { - data: { - 'span.subtype': 'http', - 'span.destination.service.resource': '93.184.216.34:80', - 'span.type': 'external', - id: '>93.184.216.34:80', - label: '93.184.216.34:80', + { + data: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, }, + ], + }); + }); + }); + }); + + describe('/api/apm/service-map/service/{serviceName}', () => { + describe('when there is no data', () => { + it('returns an object with nulls', async () => { + const q = querystring.stringify({ + start: '2020-06-28T10:24:46.055Z', + end: '2020-06-29T10:24:46.055Z', + uiFilters: {}, + }); + const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); + + expect(response.status).to.be(200); + + expect(response.body).to.eql({ + avgCpuUsage: null, + avgErrorRate: null, + avgMemoryUsage: null, + transactionStats: { + avgRequestsPerMinute: null, + avgTransactionDuration: null, }, - ], + }); }); }); });