filtering inventory page by transaction type (#86434)

* filtering inventory page by transaction type

* addressing pr comments

* addressing pr comments

* addressing pr comments

* addressing pr comments

* addressing pr comments

* addressing pr comments

* addressing pr comments

* fixing test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2020-12-24 09:36:58 +01:00 committed by GitHub
parent 6b257bb6c3
commit 6fc041c1d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 627 additions and 933 deletions

View file

@ -1,34 +0,0 @@
{
"items": [
{
"serviceName": "opbeans-node",
"agentName": "nodejs",
"transactionsPerMinute": {
"value": 0,
"timeseries": []
},
"errorsPerMinute": {
"value": 46.06666666666667,
"timeseries": []
},
"environments": ["test"]
},
{
"serviceName": "opbeans-python",
"agentName": "python",
"transactionsPerMinute": {
"value": 86.93333333333334,
"timeseries": []
},
"errorsPerMinute": {
"value": 12.6,
"timeseries": []
},
"avgResponseTime": {
"value": 91535.42944785276,
"timeseries": []
},
"environments": []
}
]
}

View file

@ -0,0 +1,29 @@
/*
* 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 { APIReturnType } from '../../../../../services/rest/createCallApmApi';
type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>;
export const items: ServiceListAPIResponse['items'] = [
{
serviceName: 'opbeans-node',
transactionType: 'request',
agentName: 'nodejs',
transactionsPerMinute: { value: 0, timeseries: [] },
transactionErrorRate: { value: 46.06666666666667, timeseries: [] },
avgResponseTime: { value: null, timeseries: [] },
environments: ['test'],
},
{
serviceName: 'opbeans-python',
transactionType: 'page-load',
agentName: 'python',
transactionsPerMinute: { value: 86.93333333333334, timeseries: [] },
transactionErrorRate: { value: 12.6, timeseries: [] },
avgResponseTime: { value: 91535.42944785276, timeseries: [] },
environments: [],
},
];

View file

@ -6,10 +6,16 @@
import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { ValuesType } from 'utility-types';
import { orderBy } from 'lodash';
import { EuiIcon } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
} from '../../../../../common/transaction_types';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import {
@ -55,126 +61,6 @@ const ToolTipWrapper = styled.span`
}
`;
export const SERVICE_COLUMNS: Array<ITableColumn<ServiceListItem>> = [
{
field: 'healthStatus',
name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', {
defaultMessage: 'Health',
}),
width: px(unit * 6),
sortable: true,
render: (_, { healthStatus }) => {
return (
<HealthBadge
healthStatus={healthStatus ?? ServiceHealthStatus.unknown}
/>
);
},
},
{
field: 'serviceName',
name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', {
defaultMessage: 'Name',
}),
width: '40%',
sortable: true,
render: (_, { serviceName, agentName }) => (
<ToolTipWrapper>
<EuiToolTip
delay="long"
content={formatString(serviceName)}
id="service-name-tooltip"
anchorClassName="apmServiceList__serviceNameTooltip"
>
<EuiFlexGroup gutterSize="s" alignItems="center">
{agentName && (
<EuiFlexItem grow={false}>
<AgentIcon agentName={agentName} />
</EuiFlexItem>
)}
<EuiFlexItem className="apmServiceList__serviceNameContainer">
<AppLink serviceName={serviceName} className="eui-textTruncate">
{formatString(serviceName)}
</AppLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
</ToolTipWrapper>
),
},
{
field: 'environments',
name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', {
defaultMessage: 'Environment',
}),
width: px(unit * 10),
sortable: true,
render: (_, { environments }) => (
<EnvironmentBadge environments={environments ?? []} />
),
},
{
field: 'avgResponseTime',
name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', {
defaultMessage: 'Avg. response time',
}),
sortable: true,
dataType: 'number',
render: (_, { avgResponseTime }) => (
<ServiceListMetric
series={avgResponseTime?.timeseries}
color="euiColorVis1"
valueLabel={asMillisecondDuration(avgResponseTime?.value || 0)}
/>
),
align: 'left',
width: px(unit * 10),
},
{
field: 'transactionsPerMinute',
name: i18n.translate(
'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel',
{
defaultMessage: 'Trans. per minute',
}
),
sortable: true,
dataType: 'number',
render: (_, { transactionsPerMinute }) => (
<ServiceListMetric
series={transactionsPerMinute?.timeseries}
color="euiColorVis0"
valueLabel={asTransactionRate(transactionsPerMinute?.value)}
/>
),
align: 'left',
width: px(unit * 10),
},
{
field: 'transactionErrorRate',
name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', {
defaultMessage: 'Error rate %',
}),
sortable: true,
dataType: 'number',
render: (_, { transactionErrorRate }) => {
const value = transactionErrorRate?.value;
const valueLabel = asPercent(value, 1);
return (
<ServiceListMetric
series={transactionErrorRate?.timeseries}
color="euiColorVis7"
valueLabel={valueLabel}
/>
);
},
align: 'left',
width: px(unit * 10),
},
];
const SERVICE_HEALTH_STATUS_ORDER = [
ServiceHealthStatus.unknown,
ServiceHealthStatus.healthy,
@ -182,59 +68,244 @@ const SERVICE_HEALTH_STATUS_ORDER = [
ServiceHealthStatus.critical,
];
export function getServiceColumns({
showTransactionTypeColumn,
}: {
showTransactionTypeColumn: boolean;
}): Array<ITableColumn<ServiceListItem>> {
return [
{
field: 'healthStatus',
name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', {
defaultMessage: 'Health',
}),
width: px(unit * 6),
sortable: true,
render: (_, { healthStatus }) => {
return (
<HealthBadge
healthStatus={healthStatus ?? ServiceHealthStatus.unknown}
/>
);
},
},
{
field: 'serviceName',
name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', {
defaultMessage: 'Name',
}),
width: '40%',
sortable: true,
render: (_, { serviceName, agentName }) => (
<ToolTipWrapper>
<EuiToolTip
delay="long"
content={formatString(serviceName)}
id="service-name-tooltip"
anchorClassName="apmServiceList__serviceNameTooltip"
>
<EuiFlexGroup gutterSize="s" alignItems="center">
{agentName && (
<EuiFlexItem grow={false}>
<AgentIcon agentName={agentName} />
</EuiFlexItem>
)}
<EuiFlexItem className="apmServiceList__serviceNameContainer">
<AppLink serviceName={serviceName} className="eui-textTruncate">
{formatString(serviceName)}
</AppLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
</ToolTipWrapper>
),
},
{
field: 'environments',
name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', {
defaultMessage: 'Environment',
}),
width: px(unit * 10),
sortable: true,
render: (_, { environments }) => (
<EnvironmentBadge environments={environments ?? []} />
),
},
...(showTransactionTypeColumn
? [
{
field: 'transactionType',
name: i18n.translate(
'xpack.apm.servicesTable.transactionColumnLabel',
{
defaultMessage: 'Transaction type',
}
),
width: px(unit * 10),
sortable: true,
},
]
: []),
{
field: 'avgResponseTime',
name: i18n.translate(
'xpack.apm.servicesTable.avgResponseTimeColumnLabel',
{
defaultMessage: 'Avg. response time',
}
),
sortable: true,
dataType: 'number',
render: (_, { avgResponseTime }) => (
<ServiceListMetric
series={avgResponseTime?.timeseries}
color="euiColorVis1"
valueLabel={asMillisecondDuration(avgResponseTime?.value || 0)}
/>
),
align: 'left',
width: px(unit * 10),
},
{
field: 'transactionsPerMinute',
name: i18n.translate(
'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel',
{
defaultMessage: 'Trans. per minute',
}
),
sortable: true,
dataType: 'number',
render: (_, { transactionsPerMinute }) => (
<ServiceListMetric
series={transactionsPerMinute?.timeseries}
color="euiColorVis0"
valueLabel={asTransactionRate(transactionsPerMinute?.value)}
/>
),
align: 'left',
width: px(unit * 10),
},
{
field: 'transactionErrorRate',
name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', {
defaultMessage: 'Error rate %',
}),
sortable: true,
dataType: 'number',
render: (_, { transactionErrorRate }) => {
const value = transactionErrorRate?.value;
const valueLabel = asPercent(value, 1);
return (
<ServiceListMetric
series={transactionErrorRate?.timeseries}
color="euiColorVis7"
valueLabel={valueLabel}
/>
);
},
align: 'left',
width: px(unit * 10),
},
];
}
export function ServiceList({ items, noItemsMessage }: Props) {
const displayHealthStatus = items.some((item) => 'healthStatus' in item);
const showTransactionTypeColumn = items.some(
({ transactionType }) =>
transactionType !== TRANSACTION_REQUEST &&
transactionType !== TRANSACTION_PAGE_LOAD
);
const serviceColumns = useMemo(
() => getServiceColumns({ showTransactionTypeColumn }),
[showTransactionTypeColumn]
);
const columns = displayHealthStatus
? SERVICE_COLUMNS
: SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus');
? serviceColumns
: serviceColumns.filter((column) => column.field !== 'healthStatus');
const initialSortField = displayHealthStatus
? 'healthStatus'
: 'transactionsPerMinute';
return (
<ManagedTable
columns={columns}
items={items}
noItemsMessage={noItemsMessage}
initialSortField={initialSortField}
initialSortDirection="desc"
initialPageSize={50}
sortFn={(itemsToSort, sortField, sortDirection) => {
// For healthStatus, sort items by healthStatus first, then by TPM
return sortField === 'healthStatus'
? orderBy(
itemsToSort,
[
(item) => {
return item.healthStatus
? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus)
: -1;
},
(item) => item.transactionsPerMinute?.value ?? 0,
],
[sortDirection, sortDirection]
)
: orderBy(
itemsToSort,
(item) => {
switch (sortField) {
// Use `?? -1` here so `undefined` will appear after/before `0`.
// In the table this will make the "N/A" items always at the
// bottom/top.
case 'avgResponseTime':
return item.avgResponseTime?.value ?? -1;
case 'transactionsPerMinute':
return item.transactionsPerMinute?.value ?? -1;
case 'transactionErrorRate':
return item.transactionErrorRate?.value ?? -1;
default:
return item[sortField as keyof typeof item];
<EuiFlexGroup direction="column" responsive={false} alignItems="flexEnd">
<EuiFlexItem>
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={i18n.translate(
'xpack.apm.servicesTable.tooltip.metricsExplanation',
{
defaultMessage:
'Service metrics are aggregated on transaction type "request", "page-load", or the top available transaction type.',
}
},
sortDirection
);
}}
/>
)}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{i18n.translate(
'xpack.apm.servicesTable.metricsExplanationLabel',
{ defaultMessage: 'What are these metrics?' }
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<ManagedTable
columns={columns}
items={items}
noItemsMessage={noItemsMessage}
initialSortField={initialSortField}
initialSortDirection="desc"
initialPageSize={50}
sortFn={(itemsToSort, sortField, sortDirection) => {
// For healthStatus, sort items by healthStatus first, then by TPM
return sortField === 'healthStatus'
? orderBy(
itemsToSort,
[
(item) => {
return item.healthStatus
? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus)
: -1;
},
(item) => item.transactionsPerMinute?.value ?? 0,
],
[sortDirection, sortDirection]
)
: orderBy(
itemsToSort,
(item) => {
switch (sortField) {
// Use `?? -1` here so `undefined` will appear after/before `0`.
// In the table this will make the "N/A" items always at the
// bottom/top.
case 'avgResponseTime':
return item.avgResponseTime?.value ?? -1;
case 'transactionsPerMinute':
return item.transactionsPerMinute?.value ?? -1;
case 'transactionErrorRate':
return item.transactionErrorRate?.value ?? -1;
default:
return item[sortField as keyof typeof item];
}
},
sortDirection
);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -9,11 +9,8 @@ import { MemoryRouter } from 'react-router-dom';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { ServiceList, SERVICE_COLUMNS } from './';
import props from './__fixtures__/props.json';
type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>;
import { getServiceColumns, ServiceList } from './';
import { items } from './__fixtures__/service_api_mock_data';
function Wrapper({ children }: { children?: ReactNode }) {
return (
@ -36,10 +33,7 @@ describe('ServiceList', () => {
it('renders with data', () => {
expect(() =>
renderWithTheme(
<ServiceList items={props.items as ServiceListAPIResponse['items']} />,
{ wrapper: Wrapper }
)
renderWithTheme(<ServiceList items={items} />, { wrapper: Wrapper })
).not.toThrowError();
});
@ -61,9 +55,9 @@ describe('ServiceList', () => {
},
environments: ['test'],
};
const renderedColumns = SERVICE_COLUMNS.map((c) =>
c.render!(service[c.field!], service)
);
const renderedColumns = getServiceColumns({
showTransactionTypeColumn: false,
}).map((c) => c.render!(service[c.field!], service));
expect(renderedColumns[0]).toMatchInlineSnapshot(`
<HealthBadge
@ -74,24 +68,18 @@ describe('ServiceList', () => {
describe('without ML data', () => {
it('does not render the health column', () => {
const { queryByText } = renderWithTheme(
<ServiceList items={props.items as ServiceListAPIResponse['items']} />,
{
wrapper: Wrapper,
}
);
const { queryByText } = renderWithTheme(<ServiceList items={items} />, {
wrapper: Wrapper,
});
const healthHeading = queryByText('Health');
expect(healthHeading).toBeNull();
});
it('sorts by transactions per minute', async () => {
const { findByTitle } = renderWithTheme(
<ServiceList items={props.items as ServiceListAPIResponse['items']} />,
{
wrapper: Wrapper,
}
);
const { findByTitle } = renderWithTheme(<ServiceList items={items} />, {
wrapper: Wrapper,
});
expect(
await findByTitle('Trans. per minute; Sorted in descending order')
@ -103,12 +91,10 @@ describe('ServiceList', () => {
it('renders the health column', async () => {
const { findByTitle } = renderWithTheme(
<ServiceList
items={(props.items as ServiceListAPIResponse['items']).map(
(item) => ({
...item,
healthStatus: ServiceHealthStatus.warning,
})
)}
items={items.map((item) => ({
...item,
healthStatus: ServiceHealthStatus.warning,
}))}
/>,
{ wrapper: Wrapper }
);

View file

@ -18,7 +18,10 @@ export function getOutcomeAggregation({
searchAggregatedTransactions: boolean;
}) {
return {
terms: { field: EVENT_OUTCOME },
terms: {
field: EVENT_OUTCOME,
include: [EventOutcome.failure, EventOutcome.success],
},
aggs: {
// simply using the doc count to get the number of requests is not possible for transaction metrics (histograms)
// to work around this we get the number of transactions by counting the number of latency values

View file

@ -100,196 +100,27 @@ Array [
"aggs": Object {
"services": Object {
"aggs": Object {
"average": Object {
"avg": Object {
"field": "transaction.duration.us",
},
},
"timeseries": Object {
"transactionType": Object {
"aggs": Object {
"average": Object {
"agentName": Object {
"top_hits": Object {
"docvalue_fields": Array [
"agent.name",
],
"size": 1,
},
},
"avg_duration": Object {
"avg": Object {
"field": "transaction.duration.us",
},
},
},
"date_histogram": Object {
"extended_bounds": Object {
"max": 1528977600000,
"min": 1528113600000,
},
"field": "@timestamp",
"fixed_interval": "43200s",
"min_doc_count": 0,
},
},
},
"terms": Object {
"field": "service.name",
"size": 500,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"service.environment": "test",
},
},
],
},
},
"size": 0,
},
},
Object {
"apm": Object {
"events": Array [
"transaction",
"metric",
"error",
],
},
"body": Object {
"aggs": Object {
"services": Object {
"aggs": Object {
"agent_name": Object {
"top_hits": Object {
"_source": Array [
"agent.name",
],
"size": 1,
},
},
},
"terms": Object {
"field": "service.name",
"size": 500,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"service.environment": "test",
},
},
],
},
},
"size": 0,
},
},
Object {
"apm": Object {
"events": Array [
"transaction",
],
},
"body": Object {
"aggs": Object {
"services": Object {
"aggs": Object {
"count": Object {
"value_count": Object {
"field": "transaction.duration.us",
},
},
"timeseries": Object {
"aggs": Object {
"count": Object {
"value_count": Object {
"field": "transaction.duration.us",
"environments": Object {
"terms": Object {
"field": "service.environment",
"missing": "",
},
},
},
"date_histogram": Object {
"extended_bounds": Object {
"max": 1528977600000,
"min": 1528113600000,
},
"field": "@timestamp",
"fixed_interval": "43200s",
"min_doc_count": 0,
},
},
},
"terms": Object {
"field": "service.name",
"size": 500,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"service.environment": "test",
},
},
],
},
},
"size": 0,
},
},
Object {
"apm": Object {
"events": Array [
"transaction",
],
},
"body": Object {
"aggs": Object {
"services": Object {
"aggs": Object {
"outcomes": Object {
"aggs": Object {
"count": Object {
"value_count": Object {
"field": "transaction.duration.us",
},
},
},
"terms": Object {
"field": "event.outcome",
},
},
"timeseries": Object {
"aggs": Object {
"outcomes": Object {
"aggs": Object {
"count": Object {
@ -300,73 +131,62 @@ Array [
},
"terms": Object {
"field": "event.outcome",
"include": Array [
"failure",
"success",
],
},
},
"real_document_count": Object {
"value_count": Object {
"field": "transaction.duration.us",
},
},
"timeseries": Object {
"aggs": Object {
"avg_duration": Object {
"avg": Object {
"field": "transaction.duration.us",
},
},
"outcomes": Object {
"aggs": Object {
"count": Object {
"value_count": Object {
"field": "transaction.duration.us",
},
},
},
"terms": Object {
"field": "event.outcome",
"include": Array [
"failure",
"success",
],
},
},
"real_document_count": Object {
"value_count": Object {
"field": "transaction.duration.us",
},
},
},
"date_histogram": Object {
"extended_bounds": Object {
"max": 1528977600000,
"min": 1528113600000,
},
"field": "@timestamp",
"fixed_interval": "43200s",
"min_doc_count": 0,
},
},
},
"date_histogram": Object {
"extended_bounds": Object {
"max": 1528977600000,
"min": 1528113600000,
},
"field": "@timestamp",
"fixed_interval": "43200s",
"min_doc_count": 0,
},
},
},
"terms": Object {
"field": "service.name",
"size": 500,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"service.environment": "test",
},
},
Object {
"terms": Object {
"event.outcome": Array [
"failure",
"success",
],
},
},
],
},
},
"size": 0,
},
},
Object {
"apm": Object {
"events": Array [
"transaction",
"metric",
"error",
],
},
"body": Object {
"aggs": Object {
"services": Object {
"aggs": Object {
"environments": Object {
"terms": Object {
"field": "service.environment",
"size": 100,
"field": "transaction.type",
"order": Object {
"real_document_count": "desc",
},
},
},
},

View file

@ -0,0 +1,56 @@
/*
* 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 { getSeverity } from '../../../../common/anomaly_detection';
import { getServiceHealthStatus } from '../../../../common/service_health_status';
import {
getMLJobIds,
getServiceAnomalies,
} from '../../service_map/get_service_anomalies';
import {
ServicesItemsProjection,
ServicesItemsSetup,
} from './get_services_items';
interface AggregationParams {
setup: ServicesItemsSetup;
projection: ServicesItemsProjection;
searchAggregatedTransactions: boolean;
}
export const getHealthStatuses = async (
{ setup }: AggregationParams,
mlAnomaliesEnvironment?: string
) => {
if (!setup.ml) {
return [];
}
const jobIds = await getMLJobIds(
setup.ml.anomalyDetectors,
mlAnomaliesEnvironment
);
if (!jobIds.length) {
return [];
}
const anomalies = await getServiceAnomalies({
setup,
environment: mlAnomaliesEnvironment,
});
return Object.keys(anomalies.serviceAnomalies).map((serviceName) => {
const stats = anomalies.serviceAnomalies[serviceName];
const severity = getSeverity(stats.anomalyScore);
const healthStatus = getServiceHealthStatus({ severity });
return {
serviceName,
healthStatus,
};
});
};

View file

@ -0,0 +1,199 @@
/*
* 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 {
AGENT_NAME,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
} from '../../../../common/transaction_types';
import { rangeFilter } from '../../../../common/utils/range_filter';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} from '../../helpers/aggregated_transactions';
import { getBucketSize } from '../../helpers/get_bucket_size';
import {
calculateTransactionErrorPercentage,
getOutcomeAggregation,
} from '../../helpers/transaction_error_rate';
import { ServicesItemsSetup } from './get_services_items';
interface AggregationParams {
setup: ServicesItemsSetup;
searchAggregatedTransactions: boolean;
}
const MAX_NUMBER_OF_SERVICES = 500;
function calculateAvgDuration({
value,
deltaAsMinutes,
}: {
value: number;
deltaAsMinutes: number;
}) {
return value / deltaAsMinutes;
}
export async function getServiceTransactionStats({
setup,
searchAggregatedTransactions,
}: AggregationParams) {
const { apmEventClient, start, end, esFilter } = setup;
const outcomes = getOutcomeAggregation({ searchAggregatedTransactions });
const metrics = {
real_document_count: {
value_count: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
avg_duration: {
avg: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
outcomes,
};
const response = await apmEventClient.search({
apm: {
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ range: rangeFilter(start, end) },
...esFilter,
...getDocumentTypeFilterForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
},
aggs: {
services: {
terms: {
field: SERVICE_NAME,
size: MAX_NUMBER_OF_SERVICES,
},
aggs: {
transactionType: {
terms: {
field: TRANSACTION_TYPE,
order: { real_document_count: 'desc' },
},
aggs: {
...metrics,
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
missing: '',
},
},
agentName: {
top_hits: {
docvalue_fields: [AGENT_NAME] as const,
size: 1,
},
},
timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval: getBucketSize({
start,
end,
numBuckets: 20,
}).intervalString,
min_doc_count: 0,
extended_bounds: { min: start, max: end },
},
aggs: metrics,
},
},
},
},
},
},
},
});
const deltaAsMinutes = (setup.end - setup.start) / 1000 / 60;
return (
response.aggregations?.services.buckets.map((bucket) => {
const topTransactionTypeBucket =
bucket.transactionType.buckets.find(
({ key }) =>
key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
) ?? bucket.transactionType.buckets[0];
return {
serviceName: bucket.key as string,
transactionType: topTransactionTypeBucket.key as string,
environments: topTransactionTypeBucket.environments.buckets
.map((environmentBucket) => environmentBucket.key as string)
.filter(Boolean),
agentName: topTransactionTypeBucket.agentName.hits.hits[0].fields[
'agent.name'
]?.[0] as AgentName,
avgResponseTime: {
value: topTransactionTypeBucket.avg_duration.value,
timeseries: topTransactionTypeBucket.timeseries.buckets.map(
(dateBucket) => ({
x: dateBucket.key,
y: dateBucket.avg_duration.value,
})
),
},
transactionErrorRate: {
value: calculateTransactionErrorPercentage(
topTransactionTypeBucket.outcomes
),
timeseries: topTransactionTypeBucket.timeseries.buckets.map(
(dateBucket) => ({
x: dateBucket.key,
y: calculateTransactionErrorPercentage(dateBucket.outcomes),
})
),
},
transactionsPerMinute: {
value: calculateAvgDuration({
value: topTransactionTypeBucket.real_document_count.value,
deltaAsMinutes,
}),
timeseries: topTransactionTypeBucket.timeseries.buckets.map(
(dateBucket) => ({
x: dateBucket.key,
y: calculateAvgDuration({
value: dateBucket.real_document_count.value,
deltaAsMinutes,
}),
})
),
},
};
}) ?? []
);
}

View file

@ -7,14 +7,8 @@ import { Logger } from '@kbn/logging';
import { joinByKey } from '../../../../common/utils/join_by_key';
import { getServicesProjection } from '../../../projections/services';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import {
getAgentNames,
getEnvironments,
getHealthStatuses,
getTransactionDurationAverages,
getTransactionErrorRates,
getTransactionRates,
} from './get_services_items_stats';
import { getHealthStatuses } from './get_health_statuses';
import { getServiceTransactionStats } from './get_service_transaction_stats';
export type ServicesItemsSetup = Setup & SetupTimeRange;
export type ServicesItemsProjection = ReturnType<typeof getServicesProjection>;
@ -37,46 +31,23 @@ export async function getServicesItems({
searchAggregatedTransactions,
};
const [
transactionDurationAverages,
agentNames,
transactionRates,
transactionErrorRates,
environments,
healthStatuses,
] = await Promise.all([
getTransactionDurationAverages(params),
getAgentNames(params),
getTransactionRates(params),
getTransactionErrorRates(params),
getEnvironments(params),
const [transactionStats, healthStatuses] = await Promise.all([
getServiceTransactionStats(params),
getHealthStatuses(params, setup.uiFilters.environment).catch((err) => {
logger.error(err);
return [];
}),
]);
const apmServiceMetrics = joinByKey(
[
...transactionDurationAverages,
...agentNames,
...transactionRates,
...transactionErrorRates,
...environments,
],
'serviceName'
);
const apmServices = apmServiceMetrics.map(({ serviceName }) => serviceName);
const apmServices = transactionStats.map(({ serviceName }) => serviceName);
// make sure to exclude health statuses from services
// that are not found in APM data
const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) =>
apmServices.includes(serviceName)
);
const allMetrics = [...apmServiceMetrics, ...matchedHealthStatuses];
const allMetrics = [...transactionStats, ...matchedHealthStatuses];
return joinByKey(allMetrics, 'serviceName');
}

View file

@ -1,413 +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 { getServiceHealthStatus } from '../../../../common/service_health_status';
import { EventOutcome } from '../../../../common/event_outcome';
import { getSeverity } from '../../../../common/anomaly_detection';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import {
AGENT_NAME,
SERVICE_ENVIRONMENT,
EVENT_OUTCOME,
} from '../../../../common/elasticsearch_fieldnames';
import { mergeProjection } from '../../../projections/util/merge_projection';
import {
ServicesItemsSetup,
ServicesItemsProjection,
} from './get_services_items';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} from '../../helpers/aggregated_transactions';
import { getBucketSize } from '../../helpers/get_bucket_size';
import {
getMLJobIds,
getServiceAnomalies,
} from '../../service_map/get_service_anomalies';
import {
calculateTransactionErrorPercentage,
getOutcomeAggregation,
getTransactionErrorRateTimeSeries,
} from '../../helpers/transaction_error_rate';
function getDateHistogramOpts(start: number, end: number) {
return {
field: '@timestamp',
fixed_interval: getBucketSize({ start, end, numBuckets: 20 })
.intervalString,
min_doc_count: 0,
extended_bounds: { min: start, max: end },
};
}
const MAX_NUMBER_OF_SERVICES = 500;
const getDeltaAsMinutes = (setup: ServicesItemsSetup) =>
(setup.end - setup.start) / 1000 / 60;
interface AggregationParams {
setup: ServicesItemsSetup;
projection: ServicesItemsProjection;
searchAggregatedTransactions: boolean;
}
export const getTransactionDurationAverages = async ({
setup,
projection,
searchAggregatedTransactions,
}: AggregationParams) => {
const { apmEventClient, start, end } = setup;
const response = await apmEventClient.search(
mergeProjection(projection, {
apm: {
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
size: 0,
query: {
bool: {
filter: [
...projection.body.query.bool.filter,
...getDocumentTypeFilterForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
},
aggs: {
services: {
terms: {
...projection.body.aggs.services.terms,
size: MAX_NUMBER_OF_SERVICES,
},
aggs: {
average: {
avg: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
timeseries: {
date_histogram: getDateHistogramOpts(start, end),
aggs: {
average: {
avg: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
},
},
},
},
},
},
})
);
const { aggregations } = response;
if (!aggregations) {
return [];
}
return aggregations.services.buckets.map((serviceBucket) => ({
serviceName: serviceBucket.key as string,
avgResponseTime: {
value: serviceBucket.average.value,
timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({
x: dateBucket.key,
y: dateBucket.average.value,
})),
},
}));
};
export const getAgentNames = async ({
setup,
projection,
}: AggregationParams) => {
const { apmEventClient } = setup;
const response = await apmEventClient.search(
mergeProjection(projection, {
body: {
size: 0,
aggs: {
services: {
terms: {
...projection.body.aggs.services.terms,
size: MAX_NUMBER_OF_SERVICES,
},
aggs: {
agent_name: {
top_hits: {
_source: [AGENT_NAME],
size: 1,
},
},
},
},
},
},
})
);
const { aggregations } = response;
if (!aggregations) {
return [];
}
return aggregations.services.buckets.map((serviceBucket) => ({
serviceName: serviceBucket.key as string,
agentName: serviceBucket.agent_name.hits.hits[0]?._source.agent
.name as AgentName,
}));
};
export const getTransactionRates = async ({
setup,
projection,
searchAggregatedTransactions,
}: AggregationParams) => {
const { apmEventClient, start, end } = setup;
const response = await apmEventClient.search(
mergeProjection(projection, {
apm: {
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
size: 0,
query: {
bool: {
filter: [
...projection.body.query.bool.filter,
...getDocumentTypeFilterForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
},
aggs: {
services: {
terms: {
...projection.body.aggs.services.terms,
size: MAX_NUMBER_OF_SERVICES,
},
aggs: {
count: {
value_count: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
timeseries: {
date_histogram: getDateHistogramOpts(start, end),
aggs: {
count: {
value_count: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
},
},
},
},
},
},
})
);
const { aggregations } = response;
if (!aggregations) {
return [];
}
const deltaAsMinutes = getDeltaAsMinutes(setup);
return aggregations.services.buckets.map((serviceBucket) => {
const transactionsPerMinute = serviceBucket.count.value / deltaAsMinutes;
return {
serviceName: serviceBucket.key as string,
transactionsPerMinute: {
value: transactionsPerMinute,
timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({
x: dateBucket.key,
y: dateBucket.count.value / deltaAsMinutes,
})),
},
};
});
};
export const getTransactionErrorRates = async ({
setup,
projection,
searchAggregatedTransactions,
}: AggregationParams) => {
const { apmEventClient, start, end } = setup;
const outcomes = getOutcomeAggregation({ searchAggregatedTransactions });
const response = await apmEventClient.search(
mergeProjection(projection, {
apm: {
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
size: 0,
query: {
bool: {
filter: [
...projection.body.query.bool.filter,
{
terms: {
[EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success],
},
},
],
},
},
aggs: {
services: {
terms: {
...projection.body.aggs.services.terms,
size: MAX_NUMBER_OF_SERVICES,
},
aggs: {
outcomes,
timeseries: {
date_histogram: getDateHistogramOpts(start, end),
aggs: {
outcomes,
},
},
},
},
},
},
})
);
const { aggregations } = response;
if (!aggregations) {
return [];
}
return aggregations.services.buckets.map((serviceBucket) => {
const transactionErrorRate = calculateTransactionErrorPercentage(
serviceBucket.outcomes
);
return {
serviceName: serviceBucket.key as string,
transactionErrorRate: {
value: transactionErrorRate,
timeseries: getTransactionErrorRateTimeSeries(
serviceBucket.timeseries.buckets
),
},
};
});
};
export const getEnvironments = async ({
setup,
projection,
}: AggregationParams) => {
const { apmEventClient, config } = setup;
const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
const response = await apmEventClient.search(
mergeProjection(projection, {
body: {
size: 0,
aggs: {
services: {
terms: {
...projection.body.aggs.services.terms,
size: MAX_NUMBER_OF_SERVICES,
},
aggs: {
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
size: maxServiceEnvironments,
},
},
},
},
},
},
})
);
const { aggregations } = response;
if (!aggregations) {
return [];
}
return aggregations.services.buckets.map((serviceBucket) => ({
serviceName: serviceBucket.key as string,
environments: serviceBucket.environments.buckets.map(
(envBucket) => envBucket.key as string
),
}));
};
export const getHealthStatuses = async (
{ setup }: AggregationParams,
mlAnomaliesEnvironment?: string
) => {
if (!setup.ml) {
return [];
}
const jobIds = await getMLJobIds(
setup.ml.anomalyDetectors,
mlAnomaliesEnvironment
);
if (!jobIds.length) {
return [];
}
const anomalies = await getServiceAnomalies({
setup,
environment: mlAnomaliesEnvironment,
});
return Object.keys(anomalies.serviceAnomalies).map((serviceName) => {
const stats = anomalies.serviceAnomalies[serviceName];
const severity = getSeverity(stats.anomalyScore);
const healthStatus = getServiceHealthStatus({ severity });
return {
serviceName,
healthStatus,
};
});
};

View file

@ -99,21 +99,24 @@ export default function ApiTest({ getService }: FtrProviderContext) {
Array [
Object {
"avgResponseTime": Object {
"value": 556200.153101878,
"value": 420419.34550767,
},
"transactionErrorRate": Object {
"value": 0,
},
"transactionsPerMinute": Object {
"value": 117.133333333333,
"value": 45.6333333333333,
},
},
Object {
"avgResponseTime": Object {
"value": 2629229.16666667,
"value": 2382833.33333333,
},
"transactionErrorRate": Object {
"value": null,
},
"transactionsPerMinute": Object {
"value": 3.2,
"value": 0.2,
},
},
Object {
@ -151,24 +154,24 @@ export default function ApiTest({ getService }: FtrProviderContext) {
},
Object {
"avgResponseTime": Object {
"value": 563605.417040359,
"value": 24920.1052631579,
},
"transactionErrorRate": Object {
"value": 0.0210526315789474,
},
"transactionsPerMinute": Object {
"value": 7.43333333333333,
"value": 3.16666666666667,
},
},
Object {
"avgResponseTime": Object {
"value": 217138.013645224,
"value": 29542.6607142857,
},
"transactionErrorRate": Object {
"value": 0.315789473684211,
"value": 0.0357142857142857,
},
"transactionsPerMinute": Object {
"value": 17.1,
"value": 1.86666666666667,
},
},
Object {
@ -186,6 +189,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
"avgResponseTime": Object {
"value": 2319812.5,
},
"transactionErrorRate": Object {
"value": null,
},
"transactionsPerMinute": Object {
"value": 0.533333333333333,
},