[APM] Service overview: Add throughput chart (#84439)

This commit is contained in:
Søren Louv-Jansen 2020-11-30 14:20:41 +01:00 committed by GitHub
parent 923a525923
commit a2b71f8bf9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 751 additions and 162 deletions

View file

@ -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', () => {

View file

@ -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' {

View file

@ -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 });

View file

@ -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 });

View file

@ -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 });

View file

@ -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>
);
};
}

View file

@ -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 = {

View file

@ -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,

View file

@ -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>

View file

@ -72,6 +72,7 @@ describe('ServiceOverview', () => {
sort: { direction: 'desc', field: 'test field' },
},
totalItemCount: 0,
throughput: [],
},
refetch: () => {},
status: FETCH_STATUS.SUCCESS,

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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({
<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'],

View file

@ -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');
});
});
});

View file

@ -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];
}

View file

@ -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);
}

View file

@ -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 };
}

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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;
}

View file

@ -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)),
};
}

View file

@ -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

View file

@ -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({

View file

@ -16,9 +16,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont
});
describe('Services', function () {
loadTestFile(require.resolve('./services/annotations'));
loadTestFile(require.resolve('./services/top_services'));
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/transaction_types'));
});

View file

@ -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,
},
]
`;

View file

@ -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();
});
});
});
});
}