From 9b25ba16498f77570f128fcb8bf0714cf29f08fa Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 7 Dec 2020 09:58:00 +0100 Subject: [PATCH 1/9] [APM] Make sure jest script can be run from anywhere --- x-pack/plugins/apm/jest.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index a0e98eebf65cb..827e7e293cd8c 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -3,9 +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. */ +const path = require('path'); module.exports = { preset: '@kbn/test', - rootDir: '../../..', + rootDir: path.resolve(__dirname, '../../..'), roots: ['/x-pack/plugins/apm'], }; From af61fe03c87606dc2eb637e8a994bd0768526349 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 7 Dec 2020 16:20:06 +0100 Subject: [PATCH 2/9] Endpoint for service instances --- .../get_service_instance_error_stats.ts | 84 +++++++++++ ...et_service_instance_system_metric_stats.ts | 132 ++++++++++++++++++ .../get_service_instance_transaction_stats.ts | 122 ++++++++++++++++ .../services/get_service_instances/index.ts | 41 ++++++ x-pack/plugins/apm/server/routes/services.ts | 32 +++++ 5 files changed, 411 insertions(+) create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts new file mode 100644 index 0000000000000..2d5c9303c1e5c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { + SERVICE_NAME, + SERVICE_NODE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { ServiceInstanceParams } from '.'; +import { getBucketSize } from '../../helpers/get_bucket_size'; + +export async function getServiceInstanceErrorStats({ + setup, + transactionType, + serviceName, + size, +}: ServiceInstanceParams) { + const { apmEventClient, start, end } = setup; + + const { intervalString } = getBucketSize({ start, end }); + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ], + }, + }, + aggs: { + [SERVICE_NODE_NAME]: { + terms: { + field: SERVICE_NODE_NAME, + missing: SERVICE_NODE_NAME_MISSING, + size, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.[SERVICE_NODE_NAME].buckets.map( + (serviceNodeBucket) => ({ + serviceNodeName: String(serviceNodeBucket.key), + errorCount: { + value: serviceNodeBucket.doc_count, + timeseries: serviceNodeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count, + }) + ), + }, + }) + ) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts new file mode 100644 index 0000000000000..774cac449fe90 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts @@ -0,0 +1,132 @@ +/* + * 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 { rangeFilter } from '../../../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { + METRIC_CGROUP_MEMORY_LIMIT_BYTES, + METRIC_CGROUP_MEMORY_USAGE_BYTES, + METRIC_SYSTEM_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, +}: ServiceInstanceParams) { + const { apmEventClient, start, end } = setup; + + const { intervalString } = getBucketSize({ start, end }); + + const subAggs = { + memory_usage_cgroup: { + avg: { script: percentCgroupMemoryUsedScript }, + }, + memory_usage_system: { + avg: { script: percentSystemMemoryUsedScript }, + }, + cpu_usage: { avg: { field: METRIC_SYSTEM_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 } }, + ], + should: [ + { + bool: { + filter: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + }, + }, + { exists: { field: METRIC_CGROUP_MEMORY_LIMIT_BYTES } }, + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, + ], + minimum_should_match: 1, + }, + }, + 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 hasCGroupData = + serviceNodeBucket.memory_usage_cgroup.value !== null; + return { + serviceNodeName: String(serviceNodeBucket.key), + cpuUsage: { + value: serviceNodeBucket.cpu_usage.value, + timeseries: serviceNodeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.cpu_usage.value, + }) + ), + }, + memoryUsage: { + value: hasCGroupData + ? serviceNodeBucket.memory_usage_cgroup.value + : serviceNodeBucket.memory_usage_system.value, + timeseries: serviceNodeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: hasCGroupData + ? dateBucket.memory_usage_cgroup.value + : dateBucket.memory_usage_system.value, + }) + ), + }, + }; + } + ) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts new file mode 100644 index 0000000000000..a8dc0b65c6f01 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts @@ -0,0 +1,122 @@ +/* + * 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 { rangeFilter } from '../../../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { + 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, +}: ServiceInstanceParams) { + const { apmEventClient, start, end } = setup; + + const { intervalString } = getBucketSize({ start, end }); + + const field = getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ); + + const subAggs = { + count: { + value_count: { + field, + }, + }, + avg_transaction_duration: { + avg: { + 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 } }, + ], + }, + }, + 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) => ({ + serviceNodeName: String(serviceNodeBucket.key), + throughput: { + value: serviceNodeBucket.count.value, + timeseries: serviceNodeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.count.value, + }) + ), + }, + latency: { + value: serviceNodeBucket.avg_transaction_duration.value, + timeseries: serviceNodeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.avg_transaction_duration.value, + }) + ), + }, + }) + ) ?? [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts new file mode 100644 index 0000000000000..023c1fc3fb9d8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { getServiceInstanceErrorStats } from './get_service_instance_error_stats'; +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; +} + +export async function getServiceInstances( + params: Omit +) { + const paramsForSubQueries = { + ...params, + size: 50, + }; + + const [errorStats, transactionStats, systemMetricStats] = await Promise.all([ + getServiceInstanceErrorStats(paramsForSubQueries), + getServiceInstanceTransactionStats(paramsForSubQueries), + getServiceInstanceSystemMetricStats(paramsForSubQueries), + ]); + + const stats = joinByKey( + [...errorStats, ...transactionStats, ...systemMetricStats], + 'serviceNodeName' + ); + + return stats; +} diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index a82f1b64d5537..7fe1188cdb501 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -20,6 +20,7 @@ import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_trans import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getThroughput } from '../lib/services/get_throughput'; +import { getServiceInstances } from '../lib/services/get_service_instances'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', @@ -275,3 +276,34 @@ export const serviceThroughputRoute = createRoute({ }); }, }); + +export const serviceOverviewInstances = 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 }), + uiFiltersRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { transactionType } = context.params.query; + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return getServiceInstances({ + serviceName, + setup, + transactionType, + searchAggregatedTransactions, + }); + }, +}); From d8370467d1385dda5ecbaac7805d7987f40c9035 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 10 Dec 2020 17:21:29 +0100 Subject: [PATCH 3/9] [APM] Service instances table --- .../components/app/service_overview/index.tsx | 12 +- .../index.tsx | 275 ++++++++++++++++++ .../components/shared/Links/apm/APMLink.tsx | 8 +- .../shared/charts/spark_plot/index.tsx | 16 +- .../get_service_instance_error_stats.ts | 84 ------ ...et_service_instance_system_metric_stats.ts | 6 +- .../get_service_instance_transaction_stats.ts | 69 +++-- .../services/get_service_instances/index.ts | 7 +- x-pack/plugins/apm/server/routes/services.ts | 5 +- 9 files changed, 345 insertions(+), 137 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 1f6a9276b5d27..cc9227422fde3 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -23,6 +23,7 @@ import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table'; import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table'; +import { ServiceOverviewInstancesTable } from './service_overview_instances_table'; /** * The height a chart should be if it's next to a table with 5 rows and a title. @@ -120,16 +121,7 @@ export function ServiceOverview({ - -

- {i18n.translate( - 'xpack.apm.serviceOverview.instancesTableTitle', - { - defaultMessage: 'Instances', - } - )} -

-
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx new file mode 100644 index 0000000000000..d3448d66f70ef --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -0,0 +1,275 @@ +/* + * 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 { 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 { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { px, unit } from '../../../../style/variables'; +import { ServiceOverviewTableContainer } from '../service_overview_table'; +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> = [ + { + field: 'name', + name: i18n.translate( + 'xpack.apm.serviceOverview.instancesTableColumnNodeName', + { + defaultMessage: 'Node name', + } + ), + render: (_, item) => { + const { serviceNodeName } = item; + const isMissing = serviceNodeName === SERVICE_NODE_NAME_MISSING; + const text = isMissing + ? UNIDENTIFIED_SERVICE_NODES_LABEL + : serviceNodeName; + + const link = + agentName === 'java' ? ( + + {text} + + ) : ( + ({ + ...query, + kuery: isMissing + ? `NOT (service.node.name:*)` + : `service.node.name:"${item.serviceNodeName}"`, + })} + > + {text} + + ); + + return ; + }, + sortable: true, + }, + { + field: 'latencyValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.instancesTableColumnLatency', + { + defaultMessage: 'Latency', + } + ), + width: px(unit * 10), + render: (_, { latency }) => { + return ( + + ); + }, + sortable: true, + }, + { + field: 'throughputValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.instancesTableColumnThroughput', + { + defaultMessage: 'Traffic', + } + ), + width: px(unit * 10), + render: (_, { throughput }) => { + return ( + + ); + }, + sortable: true, + }, + { + field: 'errorRateValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.instancesTableColumnErrorRate', + { + defaultMessage: 'Error rate', + } + ), + width: px(unit * 8), + render: (_, { errorRate }) => { + return ( + + ); + }, + sortable: true, + }, + { + field: 'cpuUsageValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.instancesTableColumnCpuUsage', + { + defaultMessage: 'CPU usage (avg.)', + } + ), + width: px(unit * 8), + render: (_, { cpuUsage }) => { + return ( + + ); + }, + sortable: true, + }, + { + field: 'memoryUsageValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.instancesTableColumnMemoryUsage', + { + defaultMessage: 'Memory usage (avg.)', + } + ), + width: px(unit * 8), + render: (_, { memoryUsage }) => { + return ( + + ); + }, + 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 ( + + + +

+ {i18n.translate('xpack.apm.serviceOverview.instancesTableTitle', { + defaultMessage: 'All instances', + })} +

+
+
+ + + + + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 98046193e3807..012c2cce176a4 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -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 ; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index ab1e725a08dff..787bef3da8d31 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -10,11 +10,10 @@ import { ScaleType, Settings, } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import { EuiIcon } 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'; interface Props { @@ -28,18 +27,7 @@ export function SparkPlot(props: Props) { const chartTheme = useChartTheme(); if (!series || series.every((point) => point.y === null)) { - return ( - - - - - - - {NOT_AVAILABLE_LABEL} - - - - ); + return ; } return ( diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts deleted file mode 100644 index 2d5c9303c1e5c..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_error_stats.ts +++ /dev/null @@ -1,84 +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 { rangeFilter } from '../../../../common/utils/range_filter'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { - SERVICE_NAME, - SERVICE_NODE_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { ServiceInstanceParams } from '.'; -import { getBucketSize } from '../../helpers/get_bucket_size'; - -export async function getServiceInstanceErrorStats({ - setup, - transactionType, - serviceName, - size, -}: ServiceInstanceParams) { - const { apmEventClient, start, end } = setup; - - const { intervalString } = getBucketSize({ start, end }); - - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.error], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { range: rangeFilter(start, end) }, - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ], - }, - }, - aggs: { - [SERVICE_NODE_NAME]: { - terms: { - field: SERVICE_NODE_NAME, - missing: SERVICE_NODE_NAME_MISSING, - size, - }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: start, - max: end, - }, - }, - }, - }, - }, - }, - }, - }); - - return ( - response.aggregations?.[SERVICE_NODE_NAME].buckets.map( - (serviceNodeBucket) => ({ - serviceNodeName: String(serviceNodeBucket.key), - errorCount: { - value: serviceNodeBucket.doc_count, - timeseries: serviceNodeBucket.timeseries.buckets.map( - (dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.doc_count, - }) - ), - }, - }) - ) ?? [] - ); -} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts index 774cac449fe90..0dfcd781e0fee 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts @@ -27,10 +27,11 @@ export async function getServiceInstanceSystemMetricStats({ setup, serviceName, size, + numBuckets, }: ServiceInstanceParams) { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end, esFilter } = setup; - const { intervalString } = getBucketSize({ start, end }); + const { intervalString } = getBucketSize({ start, end, numBuckets }); const subAggs = { memory_usage_cgroup: { @@ -53,6 +54,7 @@ export async function getServiceInstanceSystemMetricStats({ filter: [ { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, + ...esFilter, ], should: [ { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts index a8dc0b65c6f01..4e3256f0fcf87 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts @@ -4,9 +4,11 @@ * 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, @@ -24,10 +26,11 @@ export async function getServiceInstanceTransactionStats({ serviceName, size, searchAggregatedTransactions, + numBuckets, }: ServiceInstanceParams) { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end, esFilter } = setup; - const { intervalString } = getBucketSize({ start, end }); + const { intervalString } = getBucketSize({ start, end, numBuckets }); const field = getTransactionDurationFieldForAggregatedTransactions( searchAggregatedTransactions @@ -44,6 +47,20 @@ export async function getServiceInstanceTransactionStats({ field, }, }, + failures: { + filter: { + term: { + [EVENT_OUTCOME]: EventOutcome.failure, + }, + }, + aggs: { + count: { + value_count: { + field, + }, + }, + }, + }, }; const response = await apmEventClient.search({ @@ -62,6 +79,7 @@ export async function getServiceInstanceTransactionStats({ { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, + ...esFilter, ], }, }, @@ -96,27 +114,40 @@ export async function getServiceInstanceTransactionStats({ return ( response.aggregations?.[SERVICE_NODE_NAME].buckets.map( - (serviceNodeBucket) => ({ - serviceNodeName: String(serviceNodeBucket.key), - throughput: { - value: serviceNodeBucket.count.value, - timeseries: serviceNodeBucket.timeseries.buckets.map( - (dateBucket) => ({ + (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: serviceNodeBucket.avg_transaction_duration.value, - timeseries: serviceNodeBucket.timeseries.buckets.map( - (dateBucket) => ({ + })), + }, + latency: { + value: avgTransactionDuration.value, + timeseries: timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, y: dateBucket.avg_transaction_duration.value, - }) - ), - }, - }) + })), + }, + }; + } ) ?? [] ); } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts index 023c1fc3fb9d8..d627b968344f1 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts @@ -6,7 +6,6 @@ import { joinByKey } from '../../../../common/utils/join_by_key'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getServiceInstanceErrorStats } from './get_service_instance_error_stats'; import { getServiceInstanceSystemMetricStats } from './get_service_instance_system_metric_stats'; import { getServiceInstanceTransactionStats } from './get_service_instance_transaction_stats'; @@ -16,6 +15,7 @@ export interface ServiceInstanceParams { transactionType: string; searchAggregatedTransactions: boolean; size: number; + numBuckets: number; } export async function getServiceInstances( @@ -26,14 +26,13 @@ export async function getServiceInstances( size: 50, }; - const [errorStats, transactionStats, systemMetricStats] = await Promise.all([ - getServiceInstanceErrorStats(paramsForSubQueries), + const [transactionStats, systemMetricStats] = await Promise.all([ getServiceInstanceTransactionStats(paramsForSubQueries), getServiceInstanceSystemMetricStats(paramsForSubQueries), ]); const stats = joinByKey( - [...errorStats, ...transactionStats, ...systemMetricStats], + [...transactionStats, ...systemMetricStats], 'serviceNodeName' ); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 185b46d20e30f..bba6afc332242 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -285,7 +285,7 @@ export const serviceInstancesRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ - t.type({ transactionType: t.string }), + t.type({ transactionType: t.string, numBuckets: toNumberRt }), uiFiltersRt, rangeRt, ]), @@ -294,7 +294,7 @@ export const serviceInstancesRoute = createRoute({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - const { transactionType } = context.params.query; + const { transactionType, numBuckets } = context.params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -305,6 +305,7 @@ export const serviceInstancesRoute = createRoute({ setup, transactionType, searchAggregatedTransactions, + numBuckets, }); }, }); From e2c8e914a34014222495fbcbf5f28790a7cfa870 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 14 Dec 2020 11:37:13 +0100 Subject: [PATCH 4/9] Use METRIC_SYSTEM_CPU_PERCENT for cpu usage --- ...et_service_instance_system_metric_stats.ts | 108 ++++++++++-------- 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts index 0dfcd781e0fee..4798f9410099a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts @@ -4,10 +4,10 @@ * 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_LIMIT_BYTES, METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, @@ -33,14 +33,54 @@ export async function getServiceInstanceSystemMetricStats({ 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_SYSTEM_CPU_PERCENT } }; + + function withTimeseries(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: { - avg: { script: percentCgroupMemoryUsedScript }, + filter: cgroupMemoryFilter, + aggs: withTimeseries({ script: percentCgroupMemoryUsedScript }), }, memory_usage_system: { - avg: { script: percentSystemMemoryUsedScript }, + filter: systemMemoryFilter, + aggs: withTimeseries({ script: percentSystemMemoryUsedScript }), + }, + cpu_usage: { + filter: cpuUsageFilter, + aggs: withTimeseries({ field: METRIC_SYSTEM_CPU_PERCENT }), }, - cpu_usage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, }; const response = await apmEventClient.search({ @@ -56,18 +96,7 @@ export async function getServiceInstanceSystemMetricStats({ { term: { [SERVICE_NAME]: serviceName } }, ...esFilter, ], - should: [ - { - bool: { - filter: [ - { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, - { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ], - }, - }, - { exists: { field: METRIC_CGROUP_MEMORY_LIMIT_BYTES } }, - { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, - ], + should: [cgroupMemoryFilter, systemMemoryFilter, cpuUsageFilter], minimum_should_match: 1, }, }, @@ -78,21 +107,7 @@ export async function getServiceInstanceSystemMetricStats({ 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, - }, - }, + aggs: subAggs, }, }, }, @@ -102,30 +117,31 @@ export async function getServiceInstanceSystemMetricStats({ response.aggregations?.[SERVICE_NODE_NAME].buckets.map( (serviceNodeBucket) => { const hasCGroupData = - serviceNodeBucket.memory_usage_cgroup.value !== null; + 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.value, - timeseries: serviceNodeBucket.timeseries.buckets.map( + value: serviceNodeBucket.cpu_usage.avg.value, + timeseries: serviceNodeBucket.cpu_usage.timeseries.buckets.map( (dateBucket) => ({ x: dateBucket.key, - y: dateBucket.cpu_usage.value, + y: dateBucket.avg.value, }) ), }, memoryUsage: { - value: hasCGroupData - ? serviceNodeBucket.memory_usage_cgroup.value - : serviceNodeBucket.memory_usage_system.value, - timeseries: serviceNodeBucket.timeseries.buckets.map( - (dateBucket) => ({ - x: dateBucket.key, - y: hasCGroupData - ? dateBucket.memory_usage_cgroup.value - : dateBucket.memory_usage_system.value, - }) - ), + value: serviceNodeBucket[memoryMetricsKey].avg.value, + timeseries: serviceNodeBucket[ + memoryMetricsKey + ].timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.avg.value, + })), }, }; } From 21299a779ae5dc56c5ab25477cf5e76b9bd0aa53 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 14 Dec 2020 13:40:49 +0100 Subject: [PATCH 5/9] [APM] Service overview instances table --- .../apm_api_integration/basic/tests/index.ts | 2 +- .../basic/tests/service_overview/instances.ts | 206 ++++++++++++++++++ 2 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 3e625688e2459..902f48da92b1f 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -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 () { diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts new file mode 100644 index 0000000000000..3407d038c4f95 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts @@ -0,0 +1,206 @@ +/* + * 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.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.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', + 'errorRate.value', + 'throughput.value', + 'latency.value', + ]); + + expectSnapshot(values).toMatchInline(` + Object { + "cpuUsage": Object { + "value": 0.033, + }, + "errorRate": Object { + "value": 0.16, + }, + "latency": Object { + "value": 237339.813333333, + }, + "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.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.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.235616666666667, + }, + "errorRate": Object { + "value": 0.0373134328358209, + }, + "latency": Object { + "value": 70518.9328358209, + }, + "throughput": Object { + "value": 134, + }, + } + `); + + expectSnapshot(values); + }); + }); + }); + }); +} From 1ff4c0df2890eaa95b10811731a50a87b0582768 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 14 Dec 2020 14:02:18 +0100 Subject: [PATCH 6/9] Add mem snapshot tests --- .../basic/tests/service_overview/instances.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts index 3407d038c4f95..1ae3c889d7cf5 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts @@ -75,11 +75,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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); @@ -102,6 +104,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const values = pick(item, [ 'cpuUsage.value', + 'memoryUsage.value', 'errorRate.value', 'throughput.value', 'latency.value', @@ -118,6 +121,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "latency": Object { "value": 237339.813333333, }, + "memoryUsage": Object { + "value": 0.941324615478516, + }, "throughput": Object { "value": 75, }, @@ -148,11 +154,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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); From cab7e922b6e078d2c3fc3259cdd3dd78df796fcb Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 14 Dec 2020 21:16:38 +0100 Subject: [PATCH 7/9] Update tests --- .../basic/tests/service_overview/instances.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts index 1ae3c889d7cf5..084555387a690 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts @@ -113,7 +113,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(values).toMatchInline(` Object { "cpuUsage": Object { - "value": 0.033, + "value": 0.0120166666666667, }, "errorRate": Object { "value": 0.16, @@ -192,7 +192,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(values).toMatchInline(` Object { "cpuUsage": Object { - "value": 0.235616666666667, + "value": 0.00111666666666667, }, "errorRate": Object { "value": 0.0373134328358209, From 99e2b2a538ef18c4b3013e7372ab5b7300aead83 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 15 Dec 2020 08:21:34 +0100 Subject: [PATCH 8/9] Review feedback --- .../ServiceList/ServiceListMetric.tsx | 10 +- .../index.tsx | 8 +- .../service_overview_errors_table/index.tsx | 4 +- .../index.tsx | 59 +++++----- .../index.tsx | 8 +- .../shared/charts/spark_plot/index.tsx | 109 ++++++++++++------ .../spark_plot_with_value_label/index.tsx | 54 --------- 7 files changed, 114 insertions(+), 138 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx index 77257f5af7c7e..f5dc0ca162b01 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx @@ -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 ( - - ); + return ; } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index eb20ecfc10f9d..b27941eee9beb 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -27,7 +27,7 @@ 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'; @@ -88,7 +88,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { width: px(unit * 10), render: (_, { latency }) => { return ( - { return ( - { return ( - { return ( - { const { serviceNodeName } = item; - const isMissing = serviceNodeName === SERVICE_NODE_NAME_MISSING; - const text = isMissing + const isMissingServiceNodeName = + serviceNodeName === SERVICE_NODE_NAME_MISSING; + const text = isMissingServiceNodeName ? UNIDENTIFIED_SERVICE_NODES_LABEL : serviceNodeName; - const link = - agentName === 'java' ? ( - - {text} - - ) : ( - ({ - ...query, - kuery: isMissing - ? `NOT (service.node.name:*)` - : `service.node.name:"${item.serviceNodeName}"`, - })} - > - {text} - - ); + const link = isJavaAgentName(agentName) ? ( + + {text} + + ) : ( + ({ + ...query, + kuery: isMissingServiceNodeName + ? `NOT (service.node.name:*)` + : `service.node.name:"${item.serviceNodeName}"`, + })} + > + {text} + + ); return ; }, @@ -103,7 +104,7 @@ export function ServiceOverviewInstancesTable({ serviceName }: Props) { width: px(unit * 10), render: (_, { latency }) => { return ( - { return ( - { return ( - { return ( - { return ( - { return ( - { return ( - { return ( - | 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 ; - } + const colorValue = theme.eui[color]; return ( - - - - + + + {!series || series.every((point) => point.y === null) ? ( + + ) : ( + + + + + )} + + + {valueLabel} + + ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx deleted file mode 100644 index 7ca89c5a27504..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx +++ /dev/null @@ -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 ( - - - - - - {valueLabel} - - - ); -} From be15265d25af8a30d49084ece5568278d755427b Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 15 Dec 2020 08:40:33 +0100 Subject: [PATCH 9/9] Remove instances latency distribution panel --- .../components/app/service_overview/index.tsx | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index e4f5fe8479b3a..6db5b1ae7bc7c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -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'; @@ -102,27 +95,9 @@ export function ServiceOverview({ - - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle', - { - defaultMessage: 'Instances latency distribution', - } - )} -

-
-
-
- - - - - -
+ + +