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:
parent
6b257bb6c3
commit
6fc041c1d4
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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: [],
|
||||
},
|
||||
];
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
),
|
||||
},
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue