Skip to content

Commit

Permalink
[APM] Service overview: Add throughput chart (#84439)
Browse files Browse the repository at this point in the history
sorenlouv authored Nov 30, 2020
1 parent 923a525 commit a2b71f8
Showing 26 changed files with 750 additions and 161 deletions.
36 changes: 1 addition & 35 deletions x-pack/plugins/apm/common/agent_name.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
24 changes: 0 additions & 24 deletions x-pack/plugins/apm/common/agent_name.ts
Original file line number Diff line number Diff line change
@@ -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' {
Original file line number Diff line number Diff line change
@@ -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 });
Original file line number Diff line number Diff line change
@@ -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 });
Original file line number Diff line number Diff line change
@@ -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 });
Original file line number Diff line number Diff line change
@@ -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<any>) {
return (props: any) => {
return (
<ApmServiceContextProvider>
<WrappedComponent {...props} />
</ApmServiceContextProvider>
);
};
}
Original file line number Diff line number Diff line change
@@ -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 = {
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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({
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.trafficChartTitle',
{
defaultMessage: 'Traffic',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
<ServiceOverviewThroughputChart height={chartHeight} />
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ describe('ServiceOverview', () => {
sort: { direction: 'desc', field: 'test field' },
},
totalItemCount: 0,
throughput: [],
},
refetch: () => {},
status: FETCH_STATUS.SUCCESS,
Original file line number Diff line number Diff line change
@@ -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 (
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', {
defaultMessage: 'Traffic',
})}
</h2>
</EuiTitle>
<TimeseriesChart
id="throughput"
height={height}
showAnnotations={false}
fetchStatus={status}
timeseries={[
{
data: data?.throughput ?? [],
type: 'linemark',
color: theme.eui.euiColorVis0,
title: i18n.translate(
'xpack.apm.serviceOverview.throughputChart.currentPeriodLabel',
{
defaultMessage: 'Current period',
}
),
},
]}
yLabelFormat={asTransactionRate}
/>
</EuiPanel>
);
}
Original file line number Diff line number Diff line change
@@ -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) {
<EuiFlexItem grow={1}>
<Correlations />
<LocalUIFilters {...localFiltersConfig}>
<TransactionTypeFilter
transactionTypes={serviceTransactionTypes}
/>
<TransactionTypeFilter transactionTypes={transactionTypes} />
<EuiSpacer size="m" />
<EuiHorizontalRule margin="none" />
</LocalUIFilters>
Original file line number Diff line number Diff line change
@@ -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,27 +39,33 @@ 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(
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper>
<Router history={history}>
<UrlParamsProvider>
<TransactionOverview serviceName="opbeans-python" />
<ApmServiceContextProvider>
<TransactionOverview serviceName="opbeans-python" />
</ApmServiceContextProvider>
</UrlParamsProvider>
</Router>
</MockApmPluginContextWrapper>
@@ -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'],
70 changes: 70 additions & 0 deletions x-pack/plugins/apm/public/context/apm_service_context.test.tsx
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
72 changes: 72 additions & 0 deletions x-pack/plugins/apm/public/context/apm_service_context.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<APMServiceContext.Provider
value={{ agentName, transactionType, transactionTypes }}
children={children}
/>
);
}

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];
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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 };
}
Original file line number Diff line number Diff line change
@@ -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) => {
4 changes: 2 additions & 2 deletions x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts
Original file line number Diff line number Diff line change
@@ -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) => {
28 changes: 0 additions & 28 deletions x-pack/plugins/apm/public/hooks/use_transaction_type.ts

This file was deleted.

84 changes: 84 additions & 0 deletions x-pack/plugins/apm/server/lib/services/get_throughput.ts
Original file line number Diff line number Diff line change
@@ -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<typeof fetcher>;

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)),
};
}
2 changes: 2 additions & 0 deletions x-pack/plugins/apm/server/routes/create_apm_api.ts
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions x-pack/plugins/apm/server/routes/services.ts
Original file line number Diff line number Diff line change
@@ -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({
3 changes: 2 additions & 1 deletion x-pack/test/apm_api_integration/basic/tests/index.ts
Original file line number Diff line number Diff line change
@@ -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'));
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions x-pack/test/apm_api_integration/basic/tests/services/throughput.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
}

0 comments on commit a2b71f8

Please sign in to comment.