mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
360a304358
commit
7d7ebb15bb
17 changed files with 1055 additions and 249 deletions
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label';
|
||||
import { SparkPlot } from '../../../shared/charts/spark_plot';
|
||||
|
||||
export function ServiceListMetric({
|
||||
color,
|
||||
|
@ -15,11 +15,5 @@ export function ServiceListMetric({
|
|||
series?: Array<{ x: number; y: number | null }>;
|
||||
valueLabel: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
valueLabel={valueLabel}
|
||||
series={series}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
return <SparkPlot valueLabel={valueLabel} series={series} color={color} />;
|
||||
}
|
||||
|
|
|
@ -4,14 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
import { isRumAgentName } from '../../../../common/agent_name';
|
||||
|
@ -23,6 +16,7 @@ import { TransactionErrorRateChart } from '../../shared/charts/transaction_error
|
|||
import { SearchBar } from '../../shared/search_bar';
|
||||
import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table';
|
||||
import { ServiceOverviewErrorsTable } from './service_overview_errors_table';
|
||||
import { ServiceOverviewInstancesTable } from './service_overview_instances_table';
|
||||
import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart';
|
||||
import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table';
|
||||
|
||||
|
@ -101,36 +95,9 @@ export function ServiceOverview({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={4}>
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle',
|
||||
{
|
||||
defaultMessage: 'Instances latency distribution',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={6}>
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesTableTitle',
|
||||
{
|
||||
defaultMessage: 'Instances',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiPanel>
|
||||
<ServiceOverviewInstancesTable serviceName={serviceName} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPage>
|
||||
|
|
|
@ -27,12 +27,12 @@ import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
|||
import { TableLinkFlexItem } from '../table_link_flex_item';
|
||||
import { AgentIcon } from '../../../shared/AgentIcon';
|
||||
import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
|
||||
import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label';
|
||||
import { SparkPlot } from '../../../shared/charts/spark_plot';
|
||||
import { px, unit } from '../../../../style/variables';
|
||||
import { ImpactBar } from '../../../shared/ImpactBar';
|
||||
import { ServiceOverviewLink } from '../../../shared/Links/apm/service_overview_link';
|
||||
import { SpanIcon } from '../../../shared/span_icon';
|
||||
import { ServiceOverviewTableContainer } from '../service_overview_table';
|
||||
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
|
@ -88,7 +88,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
|
|||
width: px(unit * 10),
|
||||
render: (_, { latency }) => {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
<SparkPlot
|
||||
color="euiColorVis1"
|
||||
series={latency.timeseries}
|
||||
valueLabel={asDuration(latency.value)}
|
||||
|
@ -108,7 +108,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
|
|||
width: px(unit * 10),
|
||||
render: (_, { throughput }) => {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
<SparkPlot
|
||||
compact
|
||||
color="euiColorVis0"
|
||||
series={throughput.timeseries}
|
||||
|
@ -129,7 +129,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
|
|||
width: px(unit * 10),
|
||||
render: (_, { errorRate }) => {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
<SparkPlot
|
||||
compact
|
||||
color="euiColorVis7"
|
||||
series={errorRate.timeseries}
|
||||
|
|
|
@ -13,17 +13,18 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { asInteger } from '../../../../../common/utils/formatters';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
import { callApmApi } from '../../../../services/rest/createCallApmApi';
|
||||
import { px, truncate, unit } from '../../../../style/variables';
|
||||
import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label';
|
||||
import { SparkPlot } from '../../../shared/charts/spark_plot';
|
||||
import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink';
|
||||
import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink';
|
||||
import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
|
||||
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
|
||||
import { ServiceOverviewTable } from '../service_overview_table';
|
||||
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
|
||||
import { TableLinkFlexItem } from '../table_link_flex_item';
|
||||
|
||||
interface Props {
|
||||
|
@ -123,7 +124,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
|
|||
width: px(unit * 12),
|
||||
render: (_, { occurrences }) => {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
<SparkPlot
|
||||
color="euiColorVis7"
|
||||
series={occurrences.timeseries ?? undefined}
|
||||
valueLabel={i18n.translate(
|
||||
|
@ -224,41 +225,47 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TableFetchWrapper status={status}>
|
||||
<ServiceOverviewTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [PAGE_SIZE],
|
||||
hidePerPageOptions: true,
|
||||
}}
|
||||
loading={status === FETCH_STATUS.LOADING}
|
||||
onChange={(newTableOptions: {
|
||||
page?: {
|
||||
index: number;
|
||||
};
|
||||
sort?: { field: string; direction: SortDirection };
|
||||
}) => {
|
||||
setTableOptions({
|
||||
pageIndex: newTableOptions.page?.index ?? 0,
|
||||
sort: newTableOptions.sort
|
||||
? {
|
||||
field: newTableOptions.sort.field as SortField,
|
||||
direction: newTableOptions.sort.direction,
|
||||
}
|
||||
: DEFAULT_SORT,
|
||||
});
|
||||
}}
|
||||
sorting={{
|
||||
enableAllColumns: true,
|
||||
sort: {
|
||||
direction: sort.direction,
|
||||
field: sort.field,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ServiceOverviewTableContainer
|
||||
isEmptyAndLoading={
|
||||
items.length === 0 && status === FETCH_STATUS.LOADING
|
||||
}
|
||||
>
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [PAGE_SIZE],
|
||||
hidePerPageOptions: true,
|
||||
}}
|
||||
loading={status === FETCH_STATUS.LOADING}
|
||||
onChange={(newTableOptions: {
|
||||
page?: {
|
||||
index: number;
|
||||
};
|
||||
sort?: { field: string; direction: SortDirection };
|
||||
}) => {
|
||||
setTableOptions({
|
||||
pageIndex: newTableOptions.page?.index ?? 0,
|
||||
sort: newTableOptions.sort
|
||||
? {
|
||||
field: newTableOptions.sort.field as SortField,
|
||||
direction: newTableOptions.sort.direction,
|
||||
}
|
||||
: DEFAULT_SORT,
|
||||
});
|
||||
}}
|
||||
sorting={{
|
||||
enableAllColumns: true,
|
||||
sort: {
|
||||
direction: sort.direction,
|
||||
field: sort.field,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ServiceOverviewTableContainer>
|
||||
</TableFetchWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* 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 { EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { isJavaAgentName } from '../../../../../common/agent_name';
|
||||
import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import {
|
||||
asDuration,
|
||||
asPercent,
|
||||
asTransactionRate,
|
||||
} from '../../../../../common/utils/formatters';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
import {
|
||||
APIReturnType,
|
||||
callApmApi,
|
||||
} from '../../../../services/rest/createCallApmApi';
|
||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||
import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
|
||||
import { SparkPlot } from '../../../shared/charts/spark_plot';
|
||||
import { px, unit } from '../../../../style/variables';
|
||||
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
|
||||
import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink';
|
||||
import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink';
|
||||
|
||||
type ServiceInstanceItem = ValuesType<
|
||||
APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances'>
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export function ServiceOverviewInstancesTable({ serviceName }: Props) {
|
||||
const { agentName, transactionType } = useApmServiceContext();
|
||||
|
||||
const {
|
||||
urlParams: { start, end },
|
||||
uiFilters,
|
||||
} = useUrlParams();
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ServiceInstanceItem>> = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesTableColumnNodeName',
|
||||
{
|
||||
defaultMessage: 'Node name',
|
||||
}
|
||||
),
|
||||
render: (_, item) => {
|
||||
const { serviceNodeName } = item;
|
||||
const isMissingServiceNodeName =
|
||||
serviceNodeName === SERVICE_NODE_NAME_MISSING;
|
||||
const text = isMissingServiceNodeName
|
||||
? UNIDENTIFIED_SERVICE_NODES_LABEL
|
||||
: serviceNodeName;
|
||||
|
||||
const link = isJavaAgentName(agentName) ? (
|
||||
<ServiceNodeMetricOverviewLink
|
||||
serviceName={serviceName}
|
||||
serviceNodeName={item.serviceNodeName}
|
||||
>
|
||||
{text}
|
||||
</ServiceNodeMetricOverviewLink>
|
||||
) : (
|
||||
<MetricOverviewLink
|
||||
serviceName={serviceName}
|
||||
mergeQuery={(query) => ({
|
||||
...query,
|
||||
kuery: isMissingServiceNodeName
|
||||
? `NOT (service.node.name:*)`
|
||||
: `service.node.name:"${item.serviceNodeName}"`,
|
||||
})}
|
||||
>
|
||||
{text}
|
||||
</MetricOverviewLink>
|
||||
);
|
||||
|
||||
return <TruncateWithTooltip text={text} content={link} />;
|
||||
},
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'latencyValue',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesTableColumnLatency',
|
||||
{
|
||||
defaultMessage: 'Latency',
|
||||
}
|
||||
),
|
||||
width: px(unit * 10),
|
||||
render: (_, { latency }) => {
|
||||
return (
|
||||
<SparkPlot
|
||||
color="euiColorVis1"
|
||||
series={latency?.timeseries}
|
||||
valueLabel={asDuration(latency?.value)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'throughputValue',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesTableColumnThroughput',
|
||||
{
|
||||
defaultMessage: 'Traffic',
|
||||
}
|
||||
),
|
||||
width: px(unit * 10),
|
||||
render: (_, { throughput }) => {
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
color="euiColorVis0"
|
||||
series={throughput?.timeseries}
|
||||
valueLabel={asTransactionRate(throughput?.value)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'errorRateValue',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesTableColumnErrorRate',
|
||||
{
|
||||
defaultMessage: 'Error rate',
|
||||
}
|
||||
),
|
||||
width: px(unit * 8),
|
||||
render: (_, { errorRate }) => {
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
color="euiColorVis7"
|
||||
series={errorRate?.timeseries}
|
||||
valueLabel={asPercent(errorRate?.value, 1)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'cpuUsageValue',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesTableColumnCpuUsage',
|
||||
{
|
||||
defaultMessage: 'CPU usage (avg.)',
|
||||
}
|
||||
),
|
||||
width: px(unit * 8),
|
||||
render: (_, { cpuUsage }) => {
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
color="euiColorVis2"
|
||||
series={cpuUsage?.timeseries}
|
||||
valueLabel={asPercent(cpuUsage?.value, 1)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'memoryUsageValue',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceOverview.instancesTableColumnMemoryUsage',
|
||||
{
|
||||
defaultMessage: 'Memory usage (avg.)',
|
||||
}
|
||||
),
|
||||
width: px(unit * 8),
|
||||
render: (_, { memoryUsage }) => {
|
||||
return (
|
||||
<SparkPlot
|
||||
compact
|
||||
color="euiColorVis3"
|
||||
series={memoryUsage?.timeseries}
|
||||
valueLabel={asPercent(memoryUsage?.value, 1)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const { data = [], status } = useFetcher(() => {
|
||||
if (!start || !end || !transactionType) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callApmApi({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/service_overview_instances',
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
uiFilters: JSON.stringify(uiFilters),
|
||||
numBuckets: 20,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [start, end, serviceName, transactionType, uiFilters]);
|
||||
|
||||
// need top-level sortable fields for the managed table
|
||||
const items = data.map((item) => ({
|
||||
...item,
|
||||
latencyValue: item.latency?.value ?? 0,
|
||||
throughputValue: item.throughput?.value ?? 0,
|
||||
errorRateValue: item.errorRate?.value ?? 0,
|
||||
cpuUsageValue: item.cpuUsage?.value ?? 0,
|
||||
memoryUsageValue: item.memoryUsage?.value ?? 0,
|
||||
}));
|
||||
|
||||
const isLoading =
|
||||
status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.serviceOverview.instancesTableTitle', {
|
||||
defaultMessage: 'All instances',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TableFetchWrapper status={status}>
|
||||
<ServiceOverviewTableContainer
|
||||
isEmptyAndLoading={items.length === 0 && isLoading}
|
||||
>
|
||||
<EuiInMemoryTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
allowNeutralSort={false}
|
||||
loading={isLoading}
|
||||
pagination={{
|
||||
initialPageSize: 5,
|
||||
pageSizeOptions: [5],
|
||||
hidePerPageOptions: true,
|
||||
}}
|
||||
sorting={{
|
||||
sort: {
|
||||
direction: 'desc',
|
||||
field: 'throughputValue',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ServiceOverviewTableContainer>
|
||||
</TableFetchWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -4,8 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiBasicTable, EuiBasicTableProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
/**
|
||||
|
@ -43,14 +41,3 @@ export const ServiceOverviewTableContainer = styled.div<{
|
|||
isEmptyAndLoading ? 'hidden' : 'visible'};
|
||||
}
|
||||
`;
|
||||
|
||||
export function ServiceOverviewTable<T>(props: EuiBasicTableProps<T>) {
|
||||
const { items, loading } = props;
|
||||
const isEmptyAndLoading = !!(items.length === 0 && loading);
|
||||
|
||||
return (
|
||||
<ServiceOverviewTableContainer isEmptyAndLoading={isEmptyAndLoading}>
|
||||
<EuiBasicTable {...props} />
|
||||
</ServiceOverviewTableContainer>
|
||||
);
|
||||
}
|
|
@ -15,6 +15,7 @@ import React, { useState } from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { useLatencyAggregationType } from '../../../../hooks/use_latency_Aggregation_type';
|
||||
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
|
||||
import {
|
||||
|
@ -33,9 +34,9 @@ import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDeta
|
|||
import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink';
|
||||
import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
|
||||
import { TableLinkFlexItem } from '../table_link_flex_item';
|
||||
import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label';
|
||||
import { SparkPlot } from '../../../shared/charts/spark_plot';
|
||||
import { ImpactBar } from '../../../shared/ImpactBar';
|
||||
import { ServiceOverviewTable } from '../service_overview_table';
|
||||
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
|
||||
|
||||
type ServiceTransactionGroupItem = ValuesType<
|
||||
APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/overview'>['transactionGroups']
|
||||
|
@ -208,7 +209,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
|
|||
width: px(unit * 10),
|
||||
render: (_, { latency }) => {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
<SparkPlot
|
||||
color="euiColorVis1"
|
||||
compact
|
||||
series={latency.timeseries ?? undefined}
|
||||
|
@ -228,7 +229,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
|
|||
width: px(unit * 10),
|
||||
render: (_, { throughput }) => {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
<SparkPlot
|
||||
color="euiColorVis0"
|
||||
compact
|
||||
series={throughput.timeseries ?? undefined}
|
||||
|
@ -248,7 +249,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
|
|||
width: px(unit * 8),
|
||||
render: (_, { errorRate }) => {
|
||||
return (
|
||||
<SparkPlotWithValueLabel
|
||||
<SparkPlot
|
||||
color="euiColorVis7"
|
||||
compact
|
||||
series={errorRate.timeseries ?? undefined}
|
||||
|
@ -303,41 +304,47 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) {
|
|||
<EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TableFetchWrapper status={status}>
|
||||
<ServiceOverviewTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [PAGE_SIZE],
|
||||
hidePerPageOptions: true,
|
||||
}}
|
||||
loading={status === FETCH_STATUS.LOADING}
|
||||
onChange={(newTableOptions: {
|
||||
page?: {
|
||||
index: number;
|
||||
};
|
||||
sort?: { field: string; direction: SortDirection };
|
||||
}) => {
|
||||
setTableOptions({
|
||||
pageIndex: newTableOptions.page?.index ?? 0,
|
||||
sort: newTableOptions.sort
|
||||
? {
|
||||
field: newTableOptions.sort.field as SortField,
|
||||
direction: newTableOptions.sort.direction,
|
||||
}
|
||||
: DEFAULT_SORT,
|
||||
});
|
||||
}}
|
||||
sorting={{
|
||||
enableAllColumns: true,
|
||||
sort: {
|
||||
direction: sort.direction,
|
||||
field: sort.field,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ServiceOverviewTableContainer
|
||||
isEmptyAndLoading={
|
||||
items.length === 0 && status === FETCH_STATUS.LOADING
|
||||
}
|
||||
>
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [PAGE_SIZE],
|
||||
hidePerPageOptions: true,
|
||||
}}
|
||||
loading={status === FETCH_STATUS.LOADING}
|
||||
onChange={(newTableOptions: {
|
||||
page?: {
|
||||
index: number;
|
||||
};
|
||||
sort?: { field: string; direction: SortDirection };
|
||||
}) => {
|
||||
setTableOptions({
|
||||
pageIndex: newTableOptions.page?.index ?? 0,
|
||||
sort: newTableOptions.sort
|
||||
? {
|
||||
field: newTableOptions.sort.field as SortField,
|
||||
direction: newTableOptions.sort.direction,
|
||||
}
|
||||
: DEFAULT_SORT,
|
||||
});
|
||||
}}
|
||||
sorting={{
|
||||
enableAllColumns: true,
|
||||
sort: {
|
||||
direction: sort.direction,
|
||||
field: sort.field,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ServiceOverviewTableContainer>
|
||||
</TableFetchWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -18,6 +18,7 @@ import { APMQueryParams, fromQuery, toQuery } from '../url_helpers';
|
|||
interface Props extends EuiLinkAnchorProps {
|
||||
path?: string;
|
||||
query?: APMQueryParams;
|
||||
mergeQuery?: (query: APMQueryParams) => APMQueryParams;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
|
@ -74,11 +75,14 @@ export function getAPMHref({
|
|||
});
|
||||
}
|
||||
|
||||
export function APMLink({ path = '', query, ...rest }: Props) {
|
||||
export function APMLink({ path = '', query, mergeQuery, ...rest }: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const { search } = useLocation();
|
||||
const { basePath } = core.http;
|
||||
const href = getAPMHref({ basePath, path, search, query });
|
||||
|
||||
const mergedQuery = mergeQuery ? mergeQuery(query ?? {}) : query;
|
||||
|
||||
const href = getAPMHref({ basePath, path, search, query: mergedQuery });
|
||||
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import {
|
||||
AreaSeries,
|
||||
Chart,
|
||||
|
@ -10,62 +14,81 @@ import {
|
|||
ScaleType,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { merge } from 'lodash';
|
||||
import { useChartTheme } from '../../../../../../observability/public';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { px } from '../../../../style/variables';
|
||||
import { px, unit } from '../../../../style/variables';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
|
||||
interface Props {
|
||||
color: string;
|
||||
type Color =
|
||||
| 'euiColorVis0'
|
||||
| 'euiColorVis1'
|
||||
| 'euiColorVis2'
|
||||
| 'euiColorVis3'
|
||||
| 'euiColorVis4'
|
||||
| 'euiColorVis5'
|
||||
| 'euiColorVis6'
|
||||
| 'euiColorVis7'
|
||||
| 'euiColorVis8'
|
||||
| 'euiColorVis9';
|
||||
|
||||
export function SparkPlot({
|
||||
color,
|
||||
series,
|
||||
valueLabel,
|
||||
compact,
|
||||
}: {
|
||||
color: Color;
|
||||
series?: Array<{ x: number; y: number | null }> | null;
|
||||
width: string;
|
||||
}
|
||||
valueLabel: React.ReactNode;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const defaultChartTheme = useChartTheme();
|
||||
|
||||
export function SparkPlot(props: Props) {
|
||||
const { series, color, width } = props;
|
||||
const chartTheme = useChartTheme();
|
||||
const sparkplotChartTheme = merge({}, defaultChartTheme, {
|
||||
lineSeriesStyle: {
|
||||
point: { opacity: 0 },
|
||||
},
|
||||
areaSeriesStyle: {
|
||||
point: { opacity: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
if (!series || series.every((point) => point.y === null)) {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="visLine" color="subdued" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{NOT_AVAILABLE_LABEL}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
const colorValue = theme.eui[color];
|
||||
|
||||
return (
|
||||
<Chart size={{ height: px(24), width }}>
|
||||
<Settings
|
||||
theme={merge({}, chartTheme, {
|
||||
lineSeriesStyle: {
|
||||
point: { opacity: 0 },
|
||||
},
|
||||
areaSeriesStyle: {
|
||||
point: { opacity: 0 },
|
||||
},
|
||||
})}
|
||||
showLegend={false}
|
||||
tooltip="none"
|
||||
/>
|
||||
<AreaSeries
|
||||
id="area"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
data={series}
|
||||
color={color}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
</Chart>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
{!series || series.every((point) => point.y === null) ? (
|
||||
<EuiIcon type="visLine" color="subdued" />
|
||||
) : (
|
||||
<Chart
|
||||
size={{
|
||||
height: px(24),
|
||||
width: compact ? px(unit * 3) : px(unit * 4),
|
||||
}}
|
||||
>
|
||||
<Settings
|
||||
theme={sparkplotChartTheme}
|
||||
showLegend={false}
|
||||
tooltip="none"
|
||||
/>
|
||||
<AreaSeries
|
||||
id="area"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
data={series}
|
||||
color={colorValue}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
</Chart>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ whiteSpace: 'nowrap' }}>
|
||||
{valueLabel}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,54 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { px, unit } from '../../../../../style/variables';
|
||||
import { useTheme } from '../../../../../hooks/use_theme';
|
||||
import { SparkPlot } from '../';
|
||||
|
||||
type Color =
|
||||
| 'euiColorVis0'
|
||||
| 'euiColorVis1'
|
||||
| 'euiColorVis2'
|
||||
| 'euiColorVis3'
|
||||
| 'euiColorVis4'
|
||||
| 'euiColorVis5'
|
||||
| 'euiColorVis6'
|
||||
| 'euiColorVis7'
|
||||
| 'euiColorVis8'
|
||||
| 'euiColorVis9';
|
||||
|
||||
export function SparkPlotWithValueLabel({
|
||||
color,
|
||||
series,
|
||||
valueLabel,
|
||||
compact,
|
||||
}: {
|
||||
color: Color;
|
||||
series?: Array<{ x: number; y: number | null }> | null;
|
||||
valueLabel: React.ReactNode;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const colorValue = theme.eui[color];
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SparkPlot
|
||||
series={series}
|
||||
width={compact ? px(unit * 3) : px(unit * 4)}
|
||||
color={colorValue}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ whiteSpace: 'nowrap' }}>
|
||||
{valueLabel}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 { AggregationOptionsByType } from '../../../../../../typings/elasticsearch';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
|
||||
import {
|
||||
METRIC_CGROUP_MEMORY_USAGE_BYTES,
|
||||
METRIC_PROCESS_CPU_PERCENT,
|
||||
METRIC_SYSTEM_FREE_MEMORY,
|
||||
METRIC_SYSTEM_TOTAL_MEMORY,
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { ServiceInstanceParams } from '.';
|
||||
import { getBucketSize } from '../../helpers/get_bucket_size';
|
||||
import {
|
||||
percentCgroupMemoryUsedScript,
|
||||
percentSystemMemoryUsedScript,
|
||||
} from '../../metrics/by_agent/shared/memory';
|
||||
|
||||
export async function getServiceInstanceSystemMetricStats({
|
||||
setup,
|
||||
serviceName,
|
||||
size,
|
||||
numBuckets,
|
||||
}: ServiceInstanceParams) {
|
||||
const { apmEventClient, start, end, esFilter } = setup;
|
||||
|
||||
const { intervalString } = getBucketSize({ start, end, numBuckets });
|
||||
|
||||
const systemMemoryFilter = {
|
||||
bool: {
|
||||
filter: [
|
||||
{ exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
|
||||
{ exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const cgroupMemoryFilter = {
|
||||
exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES },
|
||||
};
|
||||
|
||||
const cpuUsageFilter = { exists: { field: METRIC_PROCESS_CPU_PERCENT } };
|
||||
|
||||
function withTimeseries<T extends AggregationOptionsByType['avg']>(agg: T) {
|
||||
return {
|
||||
avg: { avg: agg },
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {
|
||||
min: start,
|
||||
max: end,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
avg: { avg: agg },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const subAggs = {
|
||||
memory_usage_cgroup: {
|
||||
filter: cgroupMemoryFilter,
|
||||
aggs: withTimeseries({ script: percentCgroupMemoryUsedScript }),
|
||||
},
|
||||
memory_usage_system: {
|
||||
filter: systemMemoryFilter,
|
||||
aggs: withTimeseries({ script: percentSystemMemoryUsedScript }),
|
||||
},
|
||||
cpu_usage: {
|
||||
filter: cpuUsageFilter,
|
||||
aggs: withTimeseries({ field: METRIC_PROCESS_CPU_PERCENT }),
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search({
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: rangeFilter(start, end) },
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...esFilter,
|
||||
],
|
||||
should: [cgroupMemoryFilter, systemMemoryFilter, cpuUsageFilter],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
[SERVICE_NODE_NAME]: {
|
||||
terms: {
|
||||
field: SERVICE_NODE_NAME,
|
||||
missing: SERVICE_NODE_NAME_MISSING,
|
||||
size,
|
||||
},
|
||||
aggs: subAggs,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
response.aggregations?.[SERVICE_NODE_NAME].buckets.map(
|
||||
(serviceNodeBucket) => {
|
||||
const hasCGroupData =
|
||||
serviceNodeBucket.memory_usage_cgroup.avg.value !== null;
|
||||
|
||||
const memoryMetricsKey = hasCGroupData
|
||||
? 'memory_usage_cgroup'
|
||||
: 'memory_usage_system';
|
||||
|
||||
return {
|
||||
serviceNodeName: String(serviceNodeBucket.key),
|
||||
cpuUsage: {
|
||||
value: serviceNodeBucket.cpu_usage.avg.value,
|
||||
timeseries: serviceNodeBucket.cpu_usage.timeseries.buckets.map(
|
||||
(dateBucket) => ({
|
||||
x: dateBucket.key,
|
||||
y: dateBucket.avg.value,
|
||||
})
|
||||
),
|
||||
},
|
||||
memoryUsage: {
|
||||
value: serviceNodeBucket[memoryMetricsKey].avg.value,
|
||||
timeseries: serviceNodeBucket[
|
||||
memoryMetricsKey
|
||||
].timeseries.buckets.map((dateBucket) => ({
|
||||
x: dateBucket.key,
|
||||
y: dateBucket.avg.value,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
) ?? []
|
||||
);
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 { EventOutcome } from '../../../../common/event_outcome';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
|
||||
import {
|
||||
EVENT_OUTCOME,
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ServiceInstanceParams } from '.';
|
||||
import { getBucketSize } from '../../helpers/get_bucket_size';
|
||||
import {
|
||||
getProcessorEventForAggregatedTransactions,
|
||||
getTransactionDurationFieldForAggregatedTransactions,
|
||||
} from '../../helpers/aggregated_transactions';
|
||||
|
||||
export async function getServiceInstanceTransactionStats({
|
||||
setup,
|
||||
transactionType,
|
||||
serviceName,
|
||||
size,
|
||||
searchAggregatedTransactions,
|
||||
numBuckets,
|
||||
}: ServiceInstanceParams) {
|
||||
const { apmEventClient, start, end, esFilter } = setup;
|
||||
|
||||
const { intervalString } = getBucketSize({ start, end, numBuckets });
|
||||
|
||||
const field = getTransactionDurationFieldForAggregatedTransactions(
|
||||
searchAggregatedTransactions
|
||||
);
|
||||
|
||||
const subAggs = {
|
||||
count: {
|
||||
value_count: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
avg_transaction_duration: {
|
||||
avg: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
failures: {
|
||||
filter: {
|
||||
term: {
|
||||
[EVENT_OUTCOME]: EventOutcome.failure,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
count: {
|
||||
value_count: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search({
|
||||
apm: {
|
||||
events: [
|
||||
getProcessorEventForAggregatedTransactions(
|
||||
searchAggregatedTransactions
|
||||
),
|
||||
],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ range: rangeFilter(start, end) },
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
{ term: { [TRANSACTION_TYPE]: transactionType } },
|
||||
...esFilter,
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
[SERVICE_NODE_NAME]: {
|
||||
terms: {
|
||||
field: SERVICE_NODE_NAME,
|
||||
missing: SERVICE_NODE_NAME_MISSING,
|
||||
size,
|
||||
},
|
||||
aggs: {
|
||||
...subAggs,
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {
|
||||
min: start,
|
||||
max: end,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
...subAggs,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
response.aggregations?.[SERVICE_NODE_NAME].buckets.map(
|
||||
(serviceNodeBucket) => {
|
||||
const {
|
||||
count,
|
||||
avg_transaction_duration: avgTransactionDuration,
|
||||
key,
|
||||
failures,
|
||||
timeseries,
|
||||
} = serviceNodeBucket;
|
||||
|
||||
return {
|
||||
serviceNodeName: String(key),
|
||||
errorRate: {
|
||||
value: failures.count.value / count.value,
|
||||
timeseries: timeseries.buckets.map((dateBucket) => ({
|
||||
x: dateBucket.key,
|
||||
y: dateBucket.failures.count.value / dateBucket.count.value,
|
||||
})),
|
||||
},
|
||||
throughput: {
|
||||
value: count.value,
|
||||
timeseries: timeseries.buckets.map((dateBucket) => ({
|
||||
x: dateBucket.key,
|
||||
y: dateBucket.count.value,
|
||||
})),
|
||||
},
|
||||
latency: {
|
||||
value: avgTransactionDuration.value,
|
||||
timeseries: timeseries.buckets.map((dateBucket) => ({
|
||||
x: dateBucket.key,
|
||||
y: dateBucket.avg_transaction_duration.value,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
) ?? []
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { joinByKey } from '../../../../common/utils/join_by_key';
|
||||
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
|
||||
import { getServiceInstanceSystemMetricStats } from './get_service_instance_system_metric_stats';
|
||||
import { getServiceInstanceTransactionStats } from './get_service_instance_transaction_stats';
|
||||
|
||||
export interface ServiceInstanceParams {
|
||||
setup: Setup & SetupTimeRange;
|
||||
serviceName: string;
|
||||
transactionType: string;
|
||||
searchAggregatedTransactions: boolean;
|
||||
size: number;
|
||||
numBuckets: number;
|
||||
}
|
||||
|
||||
export async function getServiceInstances(
|
||||
params: Omit<ServiceInstanceParams, 'size'>
|
||||
) {
|
||||
const paramsForSubQueries = {
|
||||
...params,
|
||||
size: 50,
|
||||
};
|
||||
|
||||
const [transactionStats, systemMetricStats] = await Promise.all([
|
||||
getServiceInstanceTransactionStats(paramsForSubQueries),
|
||||
getServiceInstanceSystemMetricStats(paramsForSubQueries),
|
||||
]);
|
||||
|
||||
const stats = joinByKey(
|
||||
[...transactionStats, ...systemMetricStats],
|
||||
'serviceNodeName'
|
||||
);
|
||||
|
||||
return stats;
|
||||
}
|
|
@ -24,6 +24,7 @@ import {
|
|||
serviceErrorGroupsRoute,
|
||||
serviceThroughputRoute,
|
||||
serviceDependenciesRoute,
|
||||
serviceInstancesRoute,
|
||||
} from './services';
|
||||
import {
|
||||
agentConfigurationRoute,
|
||||
|
@ -124,6 +125,7 @@ const createApmApi = () => {
|
|||
.add(serviceErrorGroupsRoute)
|
||||
.add(serviceThroughputRoute)
|
||||
.add(serviceDependenciesRoute)
|
||||
.add(serviceInstancesRoute)
|
||||
|
||||
// Agent configuration
|
||||
.add(getSingleAgentConfigurationRoute)
|
||||
|
|
|
@ -21,6 +21,7 @@ import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'
|
|||
import { getServiceDependencies } from '../lib/services/get_service_dependencies';
|
||||
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
|
||||
import { getThroughput } from '../lib/services/get_throughput';
|
||||
import { getServiceInstances } from '../lib/services/get_service_instances';
|
||||
|
||||
export const servicesRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/services',
|
||||
|
@ -277,6 +278,38 @@ export const serviceThroughputRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceInstancesRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/service_overview_instances',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
t.type({ transactionType: t.string, numBuckets: toNumberRt }),
|
||||
uiFiltersRt,
|
||||
rangeRt,
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
const { transactionType, numBuckets } = context.params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
||||
return getServiceInstances({
|
||||
serviceName,
|
||||
setup,
|
||||
transactionType,
|
||||
searchAggregatedTransactions,
|
||||
numBuckets,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceDependenciesRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/dependencies',
|
||||
params: t.type({
|
||||
|
@ -284,8 +317,11 @@ export const serviceDependenciesRoute = createRoute({
|
|||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
t.type({
|
||||
environment: t.string,
|
||||
numBuckets: toNumberRt,
|
||||
}),
|
||||
rangeRt,
|
||||
t.type({ environment: t.string, numBuckets: toNumberRt }),
|
||||
]),
|
||||
}),
|
||||
options: {
|
||||
|
|
|
@ -23,10 +23,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont
|
|||
loadTestFile(require.resolve('./services/transaction_types'));
|
||||
});
|
||||
|
||||
// TODO: we should not have a service overview.
|
||||
describe('Service overview', function () {
|
||||
loadTestFile(require.resolve('./service_overview/error_groups'));
|
||||
loadTestFile(require.resolve('./service_overview/dependencies'));
|
||||
loadTestFile(require.resolve('./service_overview/instances'));
|
||||
});
|
||||
|
||||
describe('Settings', function () {
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* 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 url from 'url';
|
||||
import { pick, sortBy } from 'lodash';
|
||||
import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number';
|
||||
import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import archives from '../../../common/archives_metadata';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { start, end } = archives[archiveName];
|
||||
|
||||
interface Response {
|
||||
status: number;
|
||||
body: APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances'>;
|
||||
}
|
||||
|
||||
describe('Service overview instances', () => {
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response: Response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-java/service_overview_instances`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
numBuckets: 20,
|
||||
transactionType: 'request',
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
before(() => esArchiver.load(archiveName));
|
||||
after(() => esArchiver.unload(archiveName));
|
||||
|
||||
describe('fetching java data', () => {
|
||||
let response: Response;
|
||||
|
||||
beforeEach(async () => {
|
||||
response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-java/service_overview_instances`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
numBuckets: 20,
|
||||
transactionType: 'request',
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a service node item', () => {
|
||||
expect(response.body.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('returns statistics for each service node', () => {
|
||||
const item = response.body[0];
|
||||
|
||||
expect(isFiniteNumber(item.cpuUsage?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.memoryUsage?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.errorRate?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.throughput?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.latency?.value)).to.be(true);
|
||||
|
||||
expect(item.cpuUsage?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.memoryUsage?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.errorRate?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.throughput?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.latency?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
});
|
||||
|
||||
it('returns the right data', () => {
|
||||
const items = sortBy(response.body, 'serviceNodeName');
|
||||
|
||||
const serviceNodeNames = items.map((item) => item.serviceNodeName);
|
||||
|
||||
expectSnapshot(items.length).toMatchInline(`1`);
|
||||
|
||||
expectSnapshot(serviceNodeNames).toMatchInline(`
|
||||
Array [
|
||||
"02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c",
|
||||
]
|
||||
`);
|
||||
|
||||
const item = items[0];
|
||||
|
||||
const values = pick(item, [
|
||||
'cpuUsage.value',
|
||||
'memoryUsage.value',
|
||||
'errorRate.value',
|
||||
'throughput.value',
|
||||
'latency.value',
|
||||
]);
|
||||
|
||||
expectSnapshot(values).toMatchInline(`
|
||||
Object {
|
||||
"cpuUsage": Object {
|
||||
"value": 0.0120166666666667,
|
||||
},
|
||||
"errorRate": Object {
|
||||
"value": 0.16,
|
||||
},
|
||||
"latency": Object {
|
||||
"value": 237339.813333333,
|
||||
},
|
||||
"memoryUsage": Object {
|
||||
"value": 0.941324615478516,
|
||||
},
|
||||
"throughput": Object {
|
||||
"value": 75,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching non-java data', () => {
|
||||
let response: Response;
|
||||
|
||||
beforeEach(async () => {
|
||||
response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-ruby/service_overview_instances`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
numBuckets: 20,
|
||||
transactionType: 'request',
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns statistics for each service node', () => {
|
||||
const item = response.body[0];
|
||||
|
||||
expect(isFiniteNumber(item.cpuUsage?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.memoryUsage?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.errorRate?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.throughput?.value)).to.be(true);
|
||||
expect(isFiniteNumber(item.latency?.value)).to.be(true);
|
||||
|
||||
expect(item.cpuUsage?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.memoryUsage?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.errorRate?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.throughput?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
expect(item.latency?.timeseries.some((point) => isFiniteNumber(point.y))).to.be(true);
|
||||
});
|
||||
|
||||
it('returns the right data', () => {
|
||||
const items = sortBy(response.body, 'serviceNodeName');
|
||||
|
||||
const serviceNodeNames = items.map((item) => item.serviceNodeName);
|
||||
|
||||
expectSnapshot(items.length).toMatchInline(`1`);
|
||||
|
||||
expectSnapshot(serviceNodeNames).toMatchInline(`
|
||||
Array [
|
||||
"_service_node_name_missing_",
|
||||
]
|
||||
`);
|
||||
|
||||
const item = items[0];
|
||||
|
||||
const values = pick(
|
||||
item,
|
||||
'cpuUsage.value',
|
||||
'errorRate.value',
|
||||
'throughput.value',
|
||||
'latency.value'
|
||||
);
|
||||
|
||||
expectSnapshot(values).toMatchInline(`
|
||||
Object {
|
||||
"cpuUsage": Object {
|
||||
"value": 0.00111666666666667,
|
||||
},
|
||||
"errorRate": Object {
|
||||
"value": 0.0373134328358209,
|
||||
},
|
||||
"latency": Object {
|
||||
"value": 70518.9328358209,
|
||||
},
|
||||
"throughput": Object {
|
||||
"value": 134,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expectSnapshot(values);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue