diff --git a/x-pack/plugins/apm/common/agent_name.test.ts b/x-pack/plugins/apm/common/agent_name.test.ts index f4ac2aa220e89..10afefc264ae9 100644 --- a/x-pack/plugins/apm/common/agent_name.test.ts +++ b/x-pack/plugins/apm/common/agent_name.test.ts @@ -4,43 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getFirstTransactionType, - isJavaAgentName, - isRumAgentName, -} from './agent_name'; +import { isJavaAgentName, isRumAgentName } from './agent_name'; describe('agent name helpers', () => { - describe('getFirstTransactionType', () => { - describe('with no transaction types', () => { - expect(getFirstTransactionType([])).toBeUndefined(); - }); - - describe('with a non-rum agent', () => { - it('returns "request"', () => { - expect(getFirstTransactionType(['worker', 'request'], 'java')).toEqual( - 'request' - ); - }); - - describe('with no request types', () => { - it('returns the first type', () => { - expect( - getFirstTransactionType(['worker', 'shirker'], 'java') - ).toEqual('worker'); - }); - }); - }); - - describe('with a rum agent', () => { - it('returns "page-load"', () => { - expect( - getFirstTransactionType(['http-request', 'page-load'], 'js-base') - ).toEqual('page-load'); - }); - }); - }); - describe('isJavaAgentName', () => { describe('when the agent name is java', () => { it('returns true', () => { diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 916fe65684a6b..7fb79aa59595b 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -5,10 +5,6 @@ */ import { AgentName } from '../typings/es_schemas/ui/fields/agent'; -import { - TRANSACTION_PAGE_LOAD, - TRANSACTION_REQUEST, -} from './transaction_types'; /* * Agent names can be any string. This list only defines the official agents @@ -50,26 +46,6 @@ export const RUM_AGENT_NAMES: AgentName[] = [ 'opentelemetry/webjs', ]; -function getDefaultTransactionTypeForAgentName(agentName?: string) { - return isRumAgentName(agentName) - ? TRANSACTION_PAGE_LOAD - : TRANSACTION_REQUEST; -} - -export function getFirstTransactionType( - transactionTypes: string[], - agentName?: string -) { - const defaultTransactionType = getDefaultTransactionTypeForAgentName( - agentName - ); - - return ( - transactionTypes.find((type) => type === defaultTransactionType) ?? - transactionTypes[0] - ); -} - export function isJavaAgentName( agentName: string | undefined ): agentName is 'java' { diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx index ce98354c94c7e..b7220de8079c9 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx @@ -11,7 +11,6 @@ import React from 'react'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; @@ -22,6 +21,7 @@ import { TransactionTypeField, IsAboveField, } from '../fields'; +import { useApmService } from '../../../hooks/use_apm_service'; interface AlertParams { windowSize: number; @@ -63,7 +63,7 @@ interface Props { export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const transactionTypes = useServiceTransactionTypes(urlParams); + const { transactionTypes } = useApmService(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index 4f87e13104371..e13ed6c1bcd6f 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { ANOMALY_SEVERITY } from '../../../../../ml/common'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; @@ -24,6 +23,7 @@ import { ServiceField, TransactionTypeField, } from '../fields'; +import { useApmService } from '../../../hooks/use_apm_service'; interface Params { windowSize: number; @@ -47,7 +47,7 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const transactionTypes = useServiceTransactionTypes(urlParams); + const { transactionTypes } = useApmService(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx index a9ad212393ac4..464409ed332e8 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; @@ -19,6 +18,7 @@ import { EnvironmentField, IsAboveField, } from '../fields'; +import { useApmService } from '../../../hooks/use_apm_service'; interface AlertParams { windowSize: number; @@ -38,7 +38,7 @@ interface Props { export function TransactionErrorRateAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const transactionTypes = useServiceTransactionTypes(urlParams); + const { transactionTypes } = useApmService(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 63fb69d6d7cbf..ce8f2b0ba611a 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { ApmServiceContextProvider } from '../../../../context/apm_service_context'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; import { APMRouteDefinition } from '../../../../application/routes'; @@ -227,19 +228,19 @@ export const routes: APMRouteDefinition[] = [ breadcrumb: i18n.translate('xpack.apm.breadcrumb.overviewTitle', { defaultMessage: 'Overview', }), - component: ServiceDetailsOverview, + component: withApmServiceContext(ServiceDetailsOverview), } as APMRouteDefinition<{ serviceName: string }>, // errors { exact: true, path: '/services/:serviceName/errors/:groupId', - component: ErrorGroupDetails, + component: withApmServiceContext(ErrorGroupDetails), breadcrumb: ({ match }) => match.params.groupId, } as APMRouteDefinition<{ groupId: string; serviceName: string }>, { exact: true, path: '/services/:serviceName/errors', - component: ServiceDetailsErrors, + component: withApmServiceContext(ServiceDetailsErrors), breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', { defaultMessage: 'Errors', }), @@ -248,7 +249,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/services/:serviceName/transactions', - component: ServiceDetailsTransactions, + component: withApmServiceContext(ServiceDetailsTransactions), breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', { defaultMessage: 'Transactions', }), @@ -257,7 +258,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/services/:serviceName/metrics', - component: ServiceDetailsMetrics, + component: withApmServiceContext(ServiceDetailsMetrics), breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', { defaultMessage: 'Metrics', }), @@ -266,7 +267,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/services/:serviceName/nodes', - component: ServiceDetailsNodes, + component: withApmServiceContext(ServiceDetailsNodes), breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', { defaultMessage: 'JVMs', }), @@ -275,7 +276,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/services/:serviceName/nodes/:serviceNodeName/metrics', - component: ServiceNodeMetrics, + component: withApmServiceContext(ServiceNodeMetrics), breadcrumb: ({ match }) => { const { serviceNodeName } = match.params; @@ -289,12 +290,20 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/services/:serviceName/transactions/view', - component: TransactionDetails, + component: withApmServiceContext(TransactionDetails), breadcrumb: ({ location }) => { const query = toQuery(location.search); return query.transactionName as string; }, }, + { + exact: true, + path: '/services/:serviceName/service-map', + component: withApmServiceContext(ServiceDetailsServiceMap), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { + defaultMessage: 'Service Map', + }), + }, { exact: true, path: '/link-to/trace/:traceId', @@ -309,14 +318,6 @@ export const routes: APMRouteDefinition[] = [ defaultMessage: 'Service Map', }), }, - { - exact: true, - path: '/services/:serviceName/service-map', - component: ServiceDetailsServiceMap, - breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map', - }), - }, { exact: true, path: '/settings/customize-ui', @@ -337,3 +338,13 @@ export const routes: APMRouteDefinition[] = [ ), }, ]; + +function withApmServiceContext(WrappedComponent: React.ComponentType) { + return (props: any) => { + return ( + + + + ); + }; +} diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 92eb3753e7989..003bd6ba4c122 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { enableServiceOverview } from '../../../../common/ui_settings_keys'; -import { useAgentName } from '../../../hooks/useAgentName'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink'; import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink'; @@ -24,6 +23,7 @@ import { ServiceMetrics } from '../service_metrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; +import { useApmService } from '../../../hooks/use_apm_service'; interface Tab { key: string; @@ -44,7 +44,7 @@ interface Props { } export function ServiceDetailTabs({ serviceName, tab }: Props) { - const { agentName } = useAgentName(); + const { agentName } = useApmService(); const { uiSettings } = useApmPluginContext().core; const overviewTab = { diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index a886c3f29d57c..11de40b47ff86 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -23,10 +23,10 @@ import { RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; -import { useAgentName } from '../../../hooks/useAgentName'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmService } from '../../../hooks/use_apm_service'; import { px, truncate, unit } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; import { MetricsChart } from '../../shared/charts/metrics_chart'; @@ -58,7 +58,7 @@ type ServiceNodeMetricsProps = RouteComponentProps<{ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, serviceNodeName } = match.params; - const { agentName } = useAgentName(); + const { agentName } = useApmService(); const { data } = useServiceMetricCharts( urlParams, agentName, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index ddf3107a8ab1e..15125128d9781 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -21,6 +21,7 @@ import { TransactionErrorRateChart } from '../../shared/charts/transaction_error import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { SearchBar } from '../../shared/search_bar'; import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table'; import { TableLinkFlexItem } from './table_link_flex_item'; @@ -64,18 +65,7 @@ export function ServiceOverview({ - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.trafficChartTitle', - { - defaultMessage: 'Traffic', - } - )} -

-
-
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index e4ef7428ba8d4..b364f027538a6 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -72,6 +72,7 @@ describe('ServiceOverview', () => { sort: { direction: 'desc', field: 'test field' }, }, totalItemCount: 0, + throughput: [], }, refetch: () => {}, status: FETCH_STATUS.SUCCESS, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx new file mode 100644 index 0000000000000..94d92bfbe89dd --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -0,0 +1,80 @@ +/* + * 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 { EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { asTransactionRate } from '../../../../common/utils/formatters'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { useTheme } from '../../../hooks/useTheme'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmService } from '../../../hooks/use_apm_service'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; + +export function ServiceOverviewThroughputChart({ + height, +}: { + height?: number; +}) { + const theme = useTheme(); + const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); + const { transactionType } = useApmService(); + const { start, end } = urlParams; + + const { data, status } = useFetcher(() => { + if (serviceName && transactionType && start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName, + }, + query: { + start, + end, + transactionType, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, uiFilters, transactionType]); + + return ( + + +

+ {i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', { + defaultMessage: 'Traffic', + })} +

+
+ +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 45a6114c88afd..28a27c034265a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -24,11 +24,9 @@ import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; -import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTransactionType } from '../../../hooks/use_transaction_type'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; @@ -39,6 +37,7 @@ import { Correlations } from '../Correlations'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { UserExperienceCallout } from './user_experience_callout'; +import { useApmService } from '../../../hooks/use_apm_service'; function getRedirectLocation({ location, @@ -69,8 +68,7 @@ interface TransactionOverviewProps { export function TransactionOverview({ serviceName }: TransactionOverviewProps) { const location = useLocation(); const { urlParams } = useUrlParams(); - const transactionType = useTransactionType(); - const serviceTransactionTypes = useServiceTransactionTypes(urlParams); + const { transactionType, transactionTypes } = useApmService(); // redirect to first transaction type useRedirect(getRedirectLocation({ location, transactionType, urlParams })); @@ -122,9 +120,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { - + diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index 2d7992feb3760..d4a8b3a46991c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -11,10 +11,12 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { ApmServiceContextProvider } from '../../../context/apm_service_context'; import { UrlParamsProvider } from '../../../context/UrlParamsContext'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import * as useFetcherHook from '../../../hooks/useFetcher'; -import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes'; +import * as useServiceTransactionTypesHook from '../../../hooks/use_service_transaction_types'; +import * as useServiceAgentNameHook from '../../../hooks/use_service_agent_name'; import { disableConsoleWarning, renderWithTheme, @@ -37,19 +39,23 @@ function setup({ urlParams: IUrlParams; serviceTransactionTypes: string[]; }) { - const defaultLocation = { + history.replace({ pathname: '/services/foo/transactions', search: fromQuery(urlParams), - } as any; - - history.replace({ - ...defaultLocation, }); + // mock transaction types jest .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') .mockReturnValue(serviceTransactionTypes); + // mock agent + jest.spyOn(useServiceAgentNameHook, 'useServiceAgentName').mockReturnValue({ + agentName: 'nodejs', + error: undefined, + status: useFetcherHook.FETCH_STATUS.SUCCESS, + }); + jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); return renderWithTheme( @@ -57,7 +63,9 @@ function setup({ - + + + @@ -80,7 +88,7 @@ describe('TransactionOverview', () => { jest.clearAllMocks(); }); - describe('when no transaction type is given', () => { + describe('when no transaction type is given in urlParams', () => { it('should redirect to first type', () => { setup({ serviceTransactionTypes: ['firstType', 'secondType'], diff --git a/x-pack/plugins/apm/public/context/apm_service_context.test.tsx b/x-pack/plugins/apm/public/context/apm_service_context.test.tsx new file mode 100644 index 0000000000000..eb08cc22a0549 --- /dev/null +++ b/x-pack/plugins/apm/public/context/apm_service_context.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { getTransactionType } from './apm_service_context'; + +describe('getTransactionType', () => { + describe('with transaction type in url', () => { + it('returns the transaction type in the url ', () => { + expect( + getTransactionType({ + transactionTypes: ['worker', 'request'], + urlParams: { transactionType: 'custom' }, + agentName: 'nodejs', + }) + ).toBe('custom'); + }); + }); + + describe('with no transaction types', () => { + it('returns undefined', () => { + expect( + getTransactionType({ + transactionTypes: [], + urlParams: {}, + }) + ).toBeUndefined(); + }); + }); + + describe('with a non-rum agent', () => { + describe('with default transaction type', () => { + it('returns "request"', () => { + expect( + getTransactionType({ + transactionTypes: ['worker', 'request'], + urlParams: {}, + agentName: 'nodejs', + }) + ).toEqual('request'); + }); + }); + + describe('with no default transaction type', () => { + it('returns the first type', () => { + expect( + getTransactionType({ + transactionTypes: ['worker', 'custom'], + urlParams: {}, + agentName: 'nodejs', + }) + ).toEqual('worker'); + }); + }); + }); + + describe('with a rum agent', () => { + it('returns "page-load"', () => { + expect( + getTransactionType({ + transactionTypes: ['http-request', 'page-load'], + urlParams: {}, + agentName: 'js-base', + }) + ).toEqual('page-load'); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/context/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service_context.tsx new file mode 100644 index 0000000000000..2f1b33dea5aa6 --- /dev/null +++ b/x-pack/plugins/apm/public/context/apm_service_context.tsx @@ -0,0 +1,72 @@ +/* + * 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 React, { createContext, ReactNode } from 'react'; +import { isRumAgentName } from '../../common/agent_name'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../common/transaction_types'; +import { useServiceTransactionTypes } from '../hooks/use_service_transaction_types'; +import { useUrlParams } from '../hooks/useUrlParams'; +import { useServiceAgentName } from '../hooks/use_service_agent_name'; +import { IUrlParams } from './UrlParamsContext/types'; + +export const APMServiceContext = createContext<{ + agentName?: string; + transactionType?: string; + transactionTypes: string[]; +}>({ transactionTypes: [] }); + +export function ApmServiceContextProvider({ + children, +}: { + children: ReactNode; +}) { + const { urlParams } = useUrlParams(); + const { agentName } = useServiceAgentName(); + const transactionTypes = useServiceTransactionTypes(); + const transactionType = getTransactionType({ + urlParams, + transactionTypes, + agentName, + }); + + return ( + + ); +} + +export function getTransactionType({ + urlParams, + transactionTypes, + agentName, +}: { + urlParams: IUrlParams; + transactionTypes: string[]; + agentName?: string; +}) { + if (urlParams.transactionType) { + return urlParams.transactionType; + } + + if (!agentName || transactionTypes.length === 0) { + return; + } + + // The default transaction type is "page-load" for RUM agents and "request" for all others + const defaultTransactionType = isRumAgentName(agentName) + ? TRANSACTION_PAGE_LOAD + : TRANSACTION_REQUEST; + + // If the default transaction type is not in transactionTypes the first in the list is returned + return transactionTypes.includes(defaultTransactionType) + ? defaultTransactionType + : transactionTypes[0]; +} diff --git a/x-pack/plugins/apm/public/hooks/useComponentId.tsx b/x-pack/plugins/apm/public/hooks/use_apm_service.ts similarity index 56% rename from x-pack/plugins/apm/public/hooks/useComponentId.tsx rename to x-pack/plugins/apm/public/hooks/use_apm_service.ts index c1de5c8ba3971..bc80c3771c39d 100644 --- a/x-pack/plugins/apm/public/hooks/useComponentId.tsx +++ b/x-pack/plugins/apm/public/hooks/use_apm_service.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useRef } from 'react'; +import { useContext } from 'react'; +import { APMServiceContext } from '../context/apm_service_context'; -let uniqueId = 0; -const getUniqueId = () => uniqueId++; - -export function useComponentId() { - const idRef = useRef(getUniqueId()); - return idRef.current; +export function useApmService() { + return useContext(APMServiceContext); } diff --git a/x-pack/plugins/apm/public/hooks/useAgentName.ts b/x-pack/plugins/apm/public/hooks/use_service_agent_name.ts similarity index 81% rename from x-pack/plugins/apm/public/hooks/useAgentName.ts rename to x-pack/plugins/apm/public/hooks/use_service_agent_name.ts index b226971762fab..199f14532f7b4 100644 --- a/x-pack/plugins/apm/public/hooks/useAgentName.ts +++ b/x-pack/plugins/apm/public/hooks/use_service_agent_name.ts @@ -3,16 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { useParams } from 'react-router-dom'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; -export function useAgentName() { +export function useServiceAgentName() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; - - const { data: agentName, error, status } = useFetcher( + const { data, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ @@ -21,15 +21,11 @@ export function useAgentName() { path: { serviceName }, query: { start, end }, }, - }).then((res) => res.agentName); + }); } }, [serviceName, start, end] ); - return { - agentName, - status, - error, - }; + return { agentName: data?.agentName, status, error }; } diff --git a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx b/x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx similarity index 86% rename from x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx rename to x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx index 5f778e3d8834b..9d8892ac79b7d 100644 --- a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx +++ b/x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx @@ -5,13 +5,14 @@ */ import { useParams } from 'react-router-dom'; -import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; +import { useUrlParams } from './useUrlParams'; const INITIAL_DATA = { transactionTypes: [] }; -export function useServiceTransactionTypes(urlParams: IUrlParams) { +export function useServiceTransactionTypes() { const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams } = useUrlParams(); const { start, end } = urlParams; const { data = INITIAL_DATA } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts index 686501c1eef4c..f1671ed7aa6d9 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts @@ -7,13 +7,13 @@ import { useParams } from 'react-router-dom'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; -import { useTransactionType } from './use_transaction_type'; +import { useApmService } from './use_apm_service'; export function useTransactionBreakdown() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); const { start, end, transactionName } = urlParams; - const transactionType = useTransactionType(); + const { transactionType } = useApmService(); const { data = { timeseries: undefined }, error, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_type.ts b/x-pack/plugins/apm/public/hooks/use_transaction_type.ts deleted file mode 100644 index fd4e6516f9ca3..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_transaction_type.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { getFirstTransactionType } from '../../common/agent_name'; -import { useAgentName } from './useAgentName'; -import { useServiceTransactionTypes } from './useServiceTransactionTypes'; -import { useUrlParams } from './useUrlParams'; - -/** - * Get either the transaction type from the URL parameters, "request" - * (for non-RUM agents), "page-load" (for RUM agents) if this service uses them, - * or the first available transaction type. - */ -export function useTransactionType() { - const { agentName } = useAgentName(); - const { urlParams } = useUrlParams(); - const transactionTypeFromUrlParams = urlParams.transactionType; - const transactionTypes = useServiceTransactionTypes(urlParams); - const firstTransactionType = getFirstTransactionType( - transactionTypes, - agentName - ); - - return transactionTypeFromUrlParams ?? firstTransactionType; -} diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts new file mode 100644 index 0000000000000..0ac0ad17ef8fa --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -0,0 +1,84 @@ +/* + * 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 { PromiseReturnType } from '../../../../observability/typings/common'; +import { + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; +import { getBucketSize } from '../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; + +interface Options { + searchAggregatedTransactions: boolean; + serviceName: string; + setup: Setup & SetupTimeRange; + transactionType: string; +} + +type ESResponse = PromiseReturnType; + +function transform(response: ESResponse) { + const buckets = response.aggregations?.throughput?.buckets ?? []; + return buckets.map(({ key: x, doc_count: y }) => ({ x, y })); +} + +async function fetcher({ + searchAggregatedTransactions, + serviceName, + setup, + transactionType, +}: Options) { + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end }); + const filter: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { range: rangeFilter(start, end) }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ...setup.esFilter, + ]; + + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { bool: { filter } }, + aggs: { + throughput: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + }, + }, + }, + }; + + return apmEventClient.search(params); +} + +export async function getThroughput(options: Options) { + return { + throughput: transform(await fetcher(options)), + }; +} 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 019482dd44485..9334ce60a3f9e 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -22,6 +22,7 @@ import { serviceAnnotationsRoute, serviceAnnotationsCreateRoute, serviceErrorGroupsRoute, + serviceThroughputRoute, serviceTransactionGroupsRoute, } from './services'; import { @@ -117,6 +118,7 @@ const createApmApi = () => { .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) .add(serviceErrorGroupsRoute) + .add(serviceThroughputRoute) .add(serviceTransactionGroupsRoute) // Agent configuration diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 5e02fad2155ad..4c5738ecef581 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -20,6 +20,7 @@ import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_trans import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; +import { getThroughput } from '../lib/services/get_throughput'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', @@ -246,6 +247,36 @@ export const serviceErrorGroupsRoute = createRoute({ }, }); +export const serviceThroughputRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + t.type({ transactionType: t.string }), + uiFiltersRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { transactionType } = context.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return getThroughput({ + searchAggregatedTransactions, + serviceName, + setup, + transactionType, + }); + }, +}); + export const serviceTransactionGroupsRoute = createRoute({ endpoint: 'GET /api/apm/services/{serviceName}/overview_transaction_groups', params: t.type({ diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index e9bc59df96108..27e9528a658a9 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -16,9 +16,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont }); describe('Services', function () { + loadTestFile(require.resolve('./services/agent_name')); loadTestFile(require.resolve('./services/annotations')); + loadTestFile(require.resolve('./services/throughput')); loadTestFile(require.resolve('./services/top_services')); - loadTestFile(require.resolve('./services/agent_name')); loadTestFile(require.resolve('./services/transaction_types')); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap b/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap new file mode 100644 index 0000000000000..434660cdc2c62 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap @@ -0,0 +1,250 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Throughput when data is loaded returns the service throughput has the correct throughput 1`] = ` +Array [ + Object { + "x": 1601389800000, + "y": 6, + }, + Object { + "x": 1601389830000, + "y": 0, + }, + Object { + "x": 1601389860000, + "y": 0, + }, + Object { + "x": 1601389890000, + "y": 0, + }, + Object { + "x": 1601389920000, + "y": 3, + }, + Object { + "x": 1601389950000, + "y": 1, + }, + Object { + "x": 1601389980000, + "y": 0, + }, + Object { + "x": 1601390010000, + "y": 0, + }, + Object { + "x": 1601390040000, + "y": 3, + }, + Object { + "x": 1601390070000, + "y": 2, + }, + Object { + "x": 1601390100000, + "y": 0, + }, + Object { + "x": 1601390130000, + "y": 0, + }, + Object { + "x": 1601390160000, + "y": 7, + }, + Object { + "x": 1601390190000, + "y": 3, + }, + Object { + "x": 1601390220000, + "y": 2, + }, + Object { + "x": 1601390250000, + "y": 0, + }, + Object { + "x": 1601390280000, + "y": 0, + }, + Object { + "x": 1601390310000, + "y": 8, + }, + Object { + "x": 1601390340000, + "y": 0, + }, + Object { + "x": 1601390370000, + "y": 0, + }, + Object { + "x": 1601390400000, + "y": 3, + }, + Object { + "x": 1601390430000, + "y": 0, + }, + Object { + "x": 1601390460000, + "y": 0, + }, + Object { + "x": 1601390490000, + "y": 0, + }, + Object { + "x": 1601390520000, + "y": 4, + }, + Object { + "x": 1601390550000, + "y": 3, + }, + Object { + "x": 1601390580000, + "y": 2, + }, + Object { + "x": 1601390610000, + "y": 0, + }, + Object { + "x": 1601390640000, + "y": 1, + }, + Object { + "x": 1601390670000, + "y": 2, + }, + Object { + "x": 1601390700000, + "y": 0, + }, + Object { + "x": 1601390730000, + "y": 0, + }, + Object { + "x": 1601390760000, + "y": 4, + }, + Object { + "x": 1601390790000, + "y": 1, + }, + Object { + "x": 1601390820000, + "y": 1, + }, + Object { + "x": 1601390850000, + "y": 0, + }, + Object { + "x": 1601390880000, + "y": 6, + }, + Object { + "x": 1601390910000, + "y": 0, + }, + Object { + "x": 1601390940000, + "y": 3, + }, + Object { + "x": 1601390970000, + "y": 0, + }, + Object { + "x": 1601391000000, + "y": 4, + }, + Object { + "x": 1601391030000, + "y": 0, + }, + Object { + "x": 1601391060000, + "y": 1, + }, + Object { + "x": 1601391090000, + "y": 0, + }, + Object { + "x": 1601391120000, + "y": 2, + }, + Object { + "x": 1601391150000, + "y": 1, + }, + Object { + "x": 1601391180000, + "y": 2, + }, + Object { + "x": 1601391210000, + "y": 0, + }, + Object { + "x": 1601391240000, + "y": 1, + }, + Object { + "x": 1601391270000, + "y": 0, + }, + Object { + "x": 1601391300000, + "y": 1, + }, + Object { + "x": 1601391330000, + "y": 0, + }, + Object { + "x": 1601391360000, + "y": 1, + }, + Object { + "x": 1601391390000, + "y": 0, + }, + Object { + "x": 1601391420000, + "y": 0, + }, + Object { + "x": 1601391450000, + "y": 0, + }, + Object { + "x": 1601391480000, + "y": 10, + }, + Object { + "x": 1601391510000, + "y": 3, + }, + Object { + "x": 1601391540000, + "y": 1, + }, + Object { + "x": 1601391570000, + "y": 0, + }, + Object { + "x": 1601391600000, + "y": 0, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/services/throughput.ts b/x-pack/test/apm_api_integration/basic/tests/services/throughput.ts new file mode 100644 index 0000000000000..aea4213f7e657 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/services/throughput.ts @@ -0,0 +1,85 @@ +/* + * 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 expect from '@kbn/expect'; +import qs from 'querystring'; +import { first, last } from 'lodash'; +import archives_metadata from '../../../common/archives_metadata'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + describe('Throughput', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/throughput?${qs.stringify({ + start: metadata.start, + end: metadata.end, + uiFilters: encodeURIComponent('{}'), + transactionType: 'request', + })}` + ); + expect(response.status).to.be(200); + expect(response.body.throughput.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + describe('returns the service throughput', () => { + let throughputResponse: { + throughput: Array<{ x: number; y: number | null }>; + }; + before(async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/throughput?${qs.stringify({ + start: metadata.start, + end: metadata.end, + uiFilters: encodeURIComponent('{}'), + transactionType: 'request', + })}` + ); + throughputResponse = response.body; + }); + + it('returns some data', () => { + expect(throughputResponse.throughput.length).to.be.greaterThan(0); + + const nonNullDataPoints = throughputResponse.throughput.filter(({ y }) => y !== null); + + expect(nonNullDataPoints.length).to.be.greaterThan(0); + }); + + it('has the correct start date', () => { + expectSnapshot( + new Date(first(throughputResponse.throughput)?.x ?? NaN).toISOString() + ).toMatchInline(`"2020-09-29T14:30:00.000Z"`); + }); + + it('has the correct end date', () => { + expectSnapshot( + new Date(last(throughputResponse.throughput)?.x ?? NaN).toISOString() + ).toMatchInline(`"2020-09-29T15:00:00.000Z"`); + }); + + it('has the correct number of buckets', () => { + expectSnapshot(throughputResponse.throughput.length).toMatchInline(`61`); + }); + + it('has the correct throughput', () => { + expectSnapshot(throughputResponse.throughput).toMatch(); + }); + }); + }); + }); +}