From eebb413bab5ccd9870fb6196de6a5c0d4bdd29cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 2 Dec 2021 13:55:50 -0500 Subject: [PATCH] [APM] Service maps: Add sparklines to the detail popover (#120021) * adding error rate and latency timeseries * adding sparklines * fixing ui * fixing spaces * adjusting error color * fixing api tests * deleting unnecessary test * changing loading spinner * addressing pr comments * fixing ci --- x-pack/plugins/apm/common/service_map.ts | 28 +- .../service_map/Popover/backend_contents.tsx | 3 +- .../app/service_map/Popover/index.tsx | 2 +- .../service_map/Popover/service_contents.tsx | 8 +- .../app/service_map/Popover/stats_list.tsx | 196 ++++---- .../app/service_map/cytoscape_options.ts | 2 +- .../server/lib/helpers/transactions/index.ts | 2 +- .../lib/transaction_groups/get_error_rate.ts | 3 + .../get_transaction_group_stats.ts | 10 +- .../chart_preview/get_transaction_duration.ts | 4 +- ...egister_transaction_duration_alert_type.ts | 4 +- .../get_service_map_backend_node_info.ts | 91 +++- .../get_service_map_service_node_info.test.ts | 96 ---- .../get_service_map_service_node_info.ts | 161 +++++-- ...ervice_instances_transaction_statistics.ts | 6 +- ...e_transaction_group_detailed_statistics.ts | 6 +- .../get_service_transaction_groups.ts | 6 +- .../get_service_transaction_stats.ts | 6 +- ...service_transaction_detailed_statistics.ts | 6 +- .../transactions/get_latency_charts/index.ts | 4 +- .../tests/service_maps/service_maps.spec.ts | 439 ++++++++++-------- 21 files changed, 615 insertions(+), 468 deletions(-) delete mode 100644 x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.test.ts diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index b8e6922414ebf..d10785d614cd3 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; +import { Coordinate } from '../typings/timeseries'; import { ServiceAnomalyStats } from './anomaly_detection'; // These should be imported, but until TypeScript 4.2 we're inlining them here. @@ -59,13 +60,28 @@ export interface Connection { } export interface NodeStats { - avgMemoryUsage?: number | null; - avgCpuUsage?: number | null; - transactionStats: { - avgTransactionDuration: number | null; - avgRequestsPerMinute: number | null; + transactionStats?: { + latency?: { + value: number | null; + timeseries?: Coordinate[]; + }; + throughput?: { + value: number | null; + timeseries?: Coordinate[]; + }; + }; + failedTransactionsRate?: { + value: number | null; + timeseries?: Coordinate[]; + }; + cpuUsage?: { + value?: number | null; + timeseries?: Coordinate[]; + }; + memoryUsage?: { + value?: number | null; + timeseries?: Coordinate[]; }; - avgErrorRate: number | null; } export const invalidLicenseMessage = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index 6b9954465f39d..b61b225c2fe94 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButton, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TypeOf } from '@kbn/typed-react-router-config'; import { METRIC_TYPE } from '@kbn/analytics'; @@ -73,6 +73,7 @@ export function BackendContents({ + {/* eslint-disable-next-line @elastic/eui/href-or-on-click*/} diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx index 5c41a1b4db7e6..10d558e648376 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx @@ -7,7 +7,12 @@ /* eslint-disable @elastic/eui/href-or-on-click */ -import { EuiButton, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiButton, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useApmParams } from '../../../../hooks/use_apm_params'; @@ -89,6 +94,7 @@ export function ServiceContents({ )} + {i18n.translate('xpack.apm.serviceMap.serviceDetailsButtonText', { diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx index b46b7a0986179..002c480503454 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx @@ -5,30 +5,23 @@ * 2.0. */ -import { EuiFlexGroup, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingChart, + EuiText, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; -import React from 'react'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import React, { useMemo } from 'react'; import { NodeStats } from '../../../../../common/service_map'; import { asDuration, asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; - -export const ItemRow = euiStyled.tr` - line-height: 2; -`; - -export const ItemTitle = euiStyled.td` - color: ${({ theme }) => theme.eui.euiTextSubduedColor}; - padding-right: 1rem; -`; - -export const ItemDescription = euiStyled.td` - text-align: right; -`; +import { Coordinate } from '../../../../../typings/timeseries'; +import { SparkPlot, Color } from '../../../shared/charts/spark_plot'; function LoadingSpinner() { return ( @@ -37,7 +30,7 @@ function LoadingSpinner() { justifyContent="spaceAround" style={{ height: 170 }} > - + ); } @@ -57,22 +50,82 @@ interface StatsListProps { data: NodeStats; } +interface Item { + title: string; + valueLabel: string | null; + timeseries?: Coordinate[]; + color: Color; +} + export function StatsList({ data, isLoading }: StatsListProps) { - const { - avgCpuUsage, - avgErrorRate, - avgMemoryUsage, - transactionStats: { avgRequestsPerMinute, avgTransactionDuration }, - } = data; + const { cpuUsage, failedTransactionsRate, memoryUsage, transactionStats } = + data; const hasData = [ - avgCpuUsage, - avgErrorRate, - avgMemoryUsage, - avgRequestsPerMinute, - avgTransactionDuration, + cpuUsage?.value, + failedTransactionsRate?.value, + memoryUsage?.value, + transactionStats?.throughput?.value, + transactionStats?.latency?.value, ].some((stat) => isNumber(stat)); + const items: Item[] = useMemo( + () => [ + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgTransDurationPopoverStat', + { + defaultMessage: 'Latency (avg.)', + } + ), + valueLabel: isNumber(transactionStats?.latency?.value) + ? asDuration(transactionStats?.latency?.value) + : null, + timeseries: transactionStats?.latency?.timeseries, + color: 'euiColorVis1', + }, + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric', + { + defaultMessage: 'Throughput (avg.)', + } + ), + valueLabel: asTransactionRate(transactionStats?.throughput?.value), + timeseries: transactionStats?.throughput?.timeseries, + color: 'euiColorVis0', + }, + { + title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { + defaultMessage: 'Failed transaction rate (avg.)', + }), + valueLabel: asPercent(failedTransactionsRate?.value, 1, ''), + timeseries: failedTransactionsRate?.timeseries, + color: 'euiColorVis7', + }, + { + title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', { + defaultMessage: 'CPU usage (avg.)', + }), + valueLabel: asPercent(cpuUsage?.value, 1, ''), + timeseries: cpuUsage?.timeseries, + color: 'euiColorVis3', + }, + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgMemoryUsagePopoverStat', + { + defaultMessage: 'Memory usage (avg.)', + } + ), + valueLabel: asPercent(memoryUsage?.value, 1, ''), + timeseries: memoryUsage?.timeseries, + color: 'euiColorVis8', + }, + ], + [cpuUsage, failedTransactionsRate, memoryUsage, transactionStats] + ); + if (isLoading) { return ; } @@ -81,59 +134,40 @@ export function StatsList({ data, isLoading }: StatsListProps) { return ; } - const items = [ - { - title: i18n.translate( - 'xpack.apm.serviceMap.avgTransDurationPopoverStat', - { - defaultMessage: 'Latency (avg.)', - } - ), - description: isNumber(avgTransactionDuration) - ? asDuration(avgTransactionDuration) - : null, - }, - { - title: i18n.translate( - 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric', - { - defaultMessage: 'Throughput (avg.)', - } - ), - description: asTransactionRate(avgRequestsPerMinute), - }, - { - title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { - defaultMessage: 'Failed transaction rate (avg.)', - }), - description: asPercent(avgErrorRate, 1, ''), - }, - { - title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', { - defaultMessage: 'CPU usage (avg.)', - }), - description: asPercent(avgCpuUsage, 1, ''), - }, - { - title: i18n.translate('xpack.apm.serviceMap.avgMemoryUsagePopoverStat', { - defaultMessage: 'Memory usage (avg.)', - }), - description: asPercent(avgMemoryUsage, 1, ''), - }, - ]; - return ( - - - {items.map(({ title, description }) => { - return description ? ( - - {title} - {description} - - ) : null; - })} - -
+ + {items.map(({ title, valueLabel, timeseries, color }) => { + if (!valueLabel) { + return null; + } + return ( + + + + + {title} + + + + {timeseries ? ( + + ) : ( +
{valueLabel}
+ )} +
+
+
+ ); + })} +
); } diff --git a/x-pack/plugins/apm/public/components/app/service_map/cytoscape_options.ts b/x-pack/plugins/apm/public/components/app/service_map/cytoscape_options.ts index 1e2386c8743fe..e137f2dbb0f78 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/cytoscape_options.ts +++ b/x-pack/plugins/apm/public/components/app/service_map/cytoscape_options.ts @@ -20,7 +20,7 @@ import { import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { iconForNode } from './icons'; -export const popoverWidth = 280; +export const popoverWidth = 350; function getServiceAnomalyStats(el: cytoscape.NodeSingular) { const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( diff --git a/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts index edaae8cadc1d5..577a7544d93ea 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transactions/index.ts @@ -82,7 +82,7 @@ export async function getSearchAggregatedTransactions({ } } -export function getTransactionDurationFieldForTransactions( +export function getDurationFieldForTransactions( searchAggregatedTransactions: boolean ) { return searchAggregatedTransactions diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 328d2da0f6df0..e1dde61bfc3ff 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -42,6 +42,7 @@ export async function getErrorRate({ searchAggregatedTransactions, start, end, + numBuckets, }: { environment: string; kuery: string; @@ -52,6 +53,7 @@ export async function getErrorRate({ searchAggregatedTransactions: boolean; start: number; end: number; + numBuckets?: number; }): Promise<{ timeseries: Coordinate[]; average: number | null; @@ -91,6 +93,7 @@ export async function getErrorRate({ start, end, searchAggregatedTransactions, + numBuckets, }).intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index fd638a6731c63..04cee211c78db 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -13,7 +13,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; -import { getTransactionDurationFieldForTransactions } from '../helpers/transactions'; +import { getDurationFieldForTransactions } from '../helpers/transactions'; import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; interface MetricParams { request: TransactionGroupRequestBase; @@ -49,9 +49,7 @@ export async function getAverages({ const params = mergeRequestWithAggs(request, { avg: { avg: { - field: getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ), + field: getDurationFieldForTransactions(searchAggregatedTransactions), }, }, }); @@ -119,9 +117,7 @@ export async function getSums({ const params = mergeRequestWithAggs(request, { sum: { sum: { - field: getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ), + field: getDurationFieldForTransactions(searchAggregatedTransactions), }, }, }); diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview/get_transaction_duration.ts index 0338f78a0a892..6b5fb0c5fb1b4 100644 --- a/x-pack/plugins/apm/server/routes/alerts/chart_preview/get_transaction_duration.ts +++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview/get_transaction_duration.ts @@ -16,7 +16,7 @@ import { AlertParams } from '../route'; import { getSearchAggregatedTransactions, getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../../lib/helpers/transactions'; import { Setup } from '../../../lib/helpers/setup_request'; @@ -55,7 +55,7 @@ export async function getTransactionDurationChartPreview({ }, }; - const transactionDurationField = getTransactionDurationFieldForTransactions( + const transactionDurationField = getDurationFieldForTransactions( searchAggregatedTransactions ); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts index 1e39b02655ada..7ad78cdff9545 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts @@ -36,7 +36,7 @@ import { environmentQuery } from '../../../common/utils/environment_query'; import { getDurationFormatter } from '../../../common/utils/formatters'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, } from '../../lib/helpers/transactions'; import { getApmIndices } from '../../routes/settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; @@ -110,7 +110,7 @@ export function registerTransactionDurationAlertType({ ? indices.metric : indices.transaction; - const field = getTransactionDurationFieldForTransactions( + const field = getDurationFieldForTransactions( searchAggregatedTransactions ); diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map_backend_node_info.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map_backend_node_info.ts index 3866bac88ef44..6922fc04f2e71 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map_backend_node_info.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_map_backend_node_info.ts @@ -16,8 +16,11 @@ import { EventOutcome } from '../../../common/event_outcome'; import { ProcessorEvent } from '../../../common/processor_event'; import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; -import { calculateThroughput } from '../../lib/helpers/calculate_throughput'; +import { calculateThroughputWithRange } from '../../lib/helpers/calculate_throughput'; import { Setup } from '../../lib/helpers/setup_request'; +import { getBucketSize } from '../../lib/helpers/get_bucket_size'; +import { getFailedTransactionRateTimeSeries } from '../../lib/helpers/transaction_error_rate'; +import { NodeStats } from '../../../common/service_map'; interface Options { setup: Setup; @@ -33,10 +36,24 @@ export function getServiceMapBackendNodeInfo({ setup, start, end, -}: Options) { +}: Options): Promise { return withApmSpan('get_service_map_backend_node_stats', async () => { const { apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); + + const subAggs = { + latency_sum: { + sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM }, + }, + count: { + sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, + }, + outcomes: { + terms: { field: EVENT_OUTCOME, include: [EventOutcome.failure] }, + }, + }; + const response = await apmEventClient.search( 'get_service_map_backend_node_stats', { @@ -55,18 +72,15 @@ export function getServiceMapBackendNodeInfo({ }, }, aggs: { - latency_sum: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, + ...subAggs, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, }, - }, - count: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, - }, - }, - [EVENT_OUTCOME]: { - terms: { field: EVENT_OUTCOME, include: [EventOutcome.failure] }, + aggs: subAggs, }, }, }, @@ -74,13 +88,13 @@ export function getServiceMapBackendNodeInfo({ ); const count = response.aggregations?.count.value ?? 0; - const errorCount = - response.aggregations?.[EVENT_OUTCOME].buckets[0]?.doc_count ?? 0; + const failedTransactionsRateCount = + response.aggregations?.outcomes.buckets[0]?.doc_count ?? 0; const latencySum = response.aggregations?.latency_sum.value ?? 0; - const avgErrorRate = errorCount / count; - const avgTransactionDuration = latencySum / count; - const avgRequestsPerMinute = calculateThroughput({ + const avgFailedTransactionsRate = failedTransactionsRateCount / count; + const latency = latencySum / count; + const throughput = calculateThroughputWithRange({ start, end, value: count, @@ -88,19 +102,48 @@ export function getServiceMapBackendNodeInfo({ if (count === 0) { return { - avgErrorRate: null, + failedTransactionsRate: undefined, transactionStats: { - avgRequestsPerMinute: null, - avgTransactionDuration: null, + throughput: undefined, + latency: undefined, }, }; } return { - avgErrorRate, + failedTransactionsRate: { + value: avgFailedTransactionsRate, + timeseries: response.aggregations?.timeseries + ? getFailedTransactionRateTimeSeries( + response.aggregations.timeseries.buckets + ) + : undefined, + }, transactionStats: { - avgRequestsPerMinute, - avgTransactionDuration, + throughput: { + value: throughput, + timeseries: response.aggregations?.timeseries.buckets.map( + (bucket) => { + return { + x: bucket.key, + y: calculateThroughputWithRange({ + start, + end, + value: bucket.doc_count ?? 0, + }), + }; + } + ), + }, + latency: { + value: latency, + timeseries: response.aggregations?.timeseries.buckets.map( + (bucket) => ({ + x: bucket.key, + y: bucket.latency_sum.value, + }) + ), + }, }, }; }); diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.test.ts deleted file mode 100644 index 9f65481d07489..0000000000000 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.test.ts +++ /dev/null @@ -1,96 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getServiceMapServiceNodeInfo } from './get_service_map_service_node_info'; -import { Setup } from '../../lib/helpers/setup_request'; -import * as getErrorRateModule from '../../lib/transaction_groups/get_error_rate'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; - -describe('getServiceMapServiceNodeInfo', () => { - describe('with no results', () => { - it('returns null data', async () => { - const setup = { - apmEventClient: { - search: () => - Promise.resolve({ - hits: { total: { value: 0 } }, - }), - }, - indices: {}, - uiFilters: {}, - } as unknown as Setup; - const serviceName = 'test service name'; - const result = await getServiceMapServiceNodeInfo({ - environment: 'test environment', - setup, - serviceName, - searchAggregatedTransactions: false, - start: 1528113600000, - end: 1528977600000, - }); - - expect(result).toEqual({ - avgCpuUsage: null, - avgErrorRate: null, - avgMemoryUsage: null, - transactionStats: { - avgRequestsPerMinute: null, - avgTransactionDuration: null, - }, - }); - }); - }); - - describe('with some results', () => { - it('returns data', async () => { - jest.spyOn(getErrorRateModule, 'getErrorRate').mockResolvedValueOnce({ - average: 0.5, - timeseries: [{ x: 1634808240000, y: 0 }], - }); - - const setup = { - apmEventClient: { - search: () => - Promise.resolve({ - hits: { - total: { value: 1 }, - }, - aggregations: { - duration: { value: null }, - avgCpuUsage: { value: null }, - avgMemoryUsage: { value: null }, - }, - }), - }, - indices: {}, - start: 1593460053026000, - end: 1593497863217000, - config: { metricsInterval: 30 }, - uiFilters: { environment: 'test environment' }, - } as unknown as Setup; - const serviceName = 'test service name'; - const result = await getServiceMapServiceNodeInfo({ - setup, - serviceName, - searchAggregatedTransactions: false, - environment: ENVIRONMENT_ALL.value, - start: 1593460053026000, - end: 1593497863217000, - }); - - expect(result).toEqual({ - avgCpuUsage: null, - avgErrorRate: 0.5, - avgMemoryUsage: null, - transactionStats: { - avgRequestsPerMinute: 0.000001586873761097901, - avgTransactionDuration: null, - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts index d6eb7729effaf..ad2ab74098c22 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts @@ -6,6 +6,7 @@ */ import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; +import { rangeQuery } from '../../../../observability/server'; import { METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_CPU_PERCENT, @@ -15,24 +16,25 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; +import { NodeStats } from '../../../common/service_map'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { rangeQuery } from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { withApmSpan } from '../../utils/with_apm_span'; +import { getBucketSizeForAggregatedTransactions } from '../../lib/helpers/get_bucket_size_for_aggregated_transactions'; +import { Setup } from '../../lib/helpers/setup_request'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; -import { Setup } from '../../lib/helpers/setup_request'; +import { getErrorRate } from '../../lib/transaction_groups/get_error_rate'; +import { withApmSpan } from '../../utils/with_apm_span'; import { percentCgroupMemoryUsedScript, percentSystemMemoryUsedScript, } from '../metrics/by_agent/shared/memory'; -import { getErrorRate } from '../../lib/transaction_groups/get_error_rate'; interface Options { setup: Setup; @@ -48,8 +50,13 @@ interface TaskParameters { filter: ESFilter[]; searchAggregatedTransactions: boolean; minutes: number; - serviceName?: string; + serviceName: string; setup: Setup; + start: number; + end: number; + intervalString: string; + bucketSize: number; + numBuckets: number; } export function getServiceMapServiceNodeInfo({ @@ -59,7 +66,7 @@ export function getServiceMapServiceNodeInfo({ searchAggregatedTransactions, start, end, -}: Options) { +}: Options): Promise { return withApmSpan('get_service_map_node_stats', async () => { const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, @@ -68,6 +75,14 @@ export function getServiceMapServiceNodeInfo({ ]; const minutes = Math.abs((end - start) / (1000 * 60)); + const numBuckets = 20; + const { intervalString, bucketSize } = + getBucketSizeForAggregatedTransactions({ + start, + end, + searchAggregatedTransactions, + numBuckets, + }); const taskParams = { environment, filter, @@ -77,34 +92,38 @@ export function getServiceMapServiceNodeInfo({ setup, start, end, + intervalString, + bucketSize, + numBuckets, }; - const [errorStats, transactionStats, cpuStats, memoryStats] = + const [failedTransactionsRate, transactionStats, cpuUsage, memoryUsage] = await Promise.all([ - getErrorStats(taskParams), + getFailedTransactionsRateStats(taskParams), getTransactionStats(taskParams), getCpuStats(taskParams), getMemoryStats(taskParams), ]); return { - ...errorStats, + failedTransactionsRate, transactionStats, - ...cpuStats, - ...memoryStats, + cpuUsage, + memoryUsage, }; }); } -async function getErrorStats({ +async function getFailedTransactionsRateStats({ setup, serviceName, environment, searchAggregatedTransactions, start, end, -}: Options) { + numBuckets, +}: TaskParameters): Promise { return withApmSpan('get_error_rate_for_service_map_node', async () => { - const { average } = await getErrorRate({ + const { average, timeseries } = await getErrorRate({ environment, setup, serviceName, @@ -112,8 +131,9 @@ async function getErrorStats({ start, end, kuery: '', + numBuckets, }); - return { avgErrorRate: average }; + return { value: average, timeseries }; }); } @@ -122,12 +142,16 @@ async function getTransactionStats({ filter, minutes, searchAggregatedTransactions, -}: TaskParameters): Promise<{ - avgTransactionDuration: number | null; - avgRequestsPerMinute: number | null; -}> { + start, + end, + intervalString, +}: TaskParameters): Promise { const { apmEventClient } = setup; + const durationField = getDurationFieldForTransactions( + searchAggregatedTransactions + ); + const params = { apm: { events: [getProcessorEventForTransactions(searchAggregatedTransactions)], @@ -154,11 +178,16 @@ async function getTransactionStats({ }, track_total_hits: true, aggs: { - duration: { - avg: { - field: getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ), + duration: { avg: { field: durationField } }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + latency: { avg: { field: durationField } }, }, }, }, @@ -172,15 +201,32 @@ async function getTransactionStats({ const totalRequests = response.hits.total.value; return { - avgTransactionDuration: response.aggregations?.duration.value ?? null, - avgRequestsPerMinute: totalRequests > 0 ? totalRequests / minutes : null, + latency: { + value: response.aggregations?.duration.value ?? null, + timeseries: response.aggregations?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.latency.value, + })), + }, + throughput: { + value: totalRequests > 0 ? totalRequests / minutes : null, + timeseries: response.aggregations?.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + y: bucket.doc_count ?? 0, + }; + }), + }, }; } async function getCpuStats({ setup, filter, -}: TaskParameters): Promise<{ avgCpuUsage: number | null }> { + intervalString, + start, + end, +}: TaskParameters): Promise { const { apmEventClient } = setup; const response = await apmEventClient.search( @@ -199,22 +245,44 @@ async function getCpuStats({ ], }, }, - aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } }, + aggs: { + avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + cpuAvg: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, + }, + }, + }, }, } ); - return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null }; + return { + value: response.aggregations?.avgCpuUsage.value ?? null, + timeseries: response.aggregations?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.cpuAvg.value, + })), + }; } function getMemoryStats({ setup, filter, -}: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { + intervalString, + start, + end, +}: TaskParameters) { return withApmSpan('get_memory_stats_for_service_map_node', async () => { const { apmEventClient } = setup; - const getAvgMemoryUsage = async ({ + const getMemoryUsage = async ({ additionalFilters, script, }: { @@ -222,7 +290,7 @@ function getMemoryStats({ script: | typeof percentCgroupMemoryUsedScript | typeof percentSystemMemoryUsedScript; - }) => { + }): Promise => { const response = await apmEventClient.search( 'get_avg_memory_for_service_map_node', { @@ -238,22 +306,39 @@ function getMemoryStats({ }, aggs: { avgMemoryUsage: { avg: { script } }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + memoryAvg: { avg: { script } }, + }, + }, }, }, } ); - return response.aggregations?.avgMemoryUsage.value ?? null; + return { + value: response.aggregations?.avgMemoryUsage.value ?? null, + timeseries: response.aggregations?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.memoryAvg.value, + })), + }; }; - let avgMemoryUsage = await getAvgMemoryUsage({ + let memoryUsage = await getMemoryUsage({ additionalFilters: [ { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, ], script: percentCgroupMemoryUsedScript, }); - if (!avgMemoryUsage) { - avgMemoryUsage = await getAvgMemoryUsage({ + if (!memoryUsage) { + memoryUsage = await getMemoryUsage({ additionalFilters: [ { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, @@ -262,6 +347,6 @@ function getMemoryStats({ }); } - return { avgMemoryUsage }; + return memoryUsage; }); } diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts index 166e8d61142ea..ec081916f455d 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instances/get_service_instances_transaction_statistics.ts @@ -18,7 +18,7 @@ import { kqlQuery, rangeQuery } from '../../../../../observability/server'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../../lib/helpers/transactions'; import { calculateThroughput } from '../../../lib/helpers/calculate_throughput'; @@ -89,9 +89,7 @@ export async function getServiceInstancesTransactionStatistics< } ); - const field = getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ); + const field = getDurationFieldForTransactions(searchAggregatedTransactions); const subAggs = { ...getLatencyAggregation(latencyAggregationType, field), diff --git a/x-pack/plugins/apm/server/routes/services/get_service_transaction_group_detailed_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_service_transaction_group_detailed_statistics.ts index 70f77c26fdbf9..b14329985db90 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_transaction_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_transaction_group_detailed_statistics.ts @@ -20,7 +20,7 @@ import { environmentQuery } from '../../../common/utils/environment_query'; import { Coordinate } from '../../../typings/timeseries'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; import { getBucketSizeForAggregatedTransactions } from '../../lib/helpers/get_bucket_size_for_aggregated_transactions'; @@ -72,9 +72,7 @@ export async function getServiceTransactionGroupDetailedStatistics({ searchAggregatedTransactions, }); - const field = getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ); + const field = getDurationFieldForTransactions(searchAggregatedTransactions); const response = await apmEventClient.search( 'get_service_transaction_group_detailed_statistics', diff --git a/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups.ts index 711d6964221bd..979d79c84578a 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups.ts @@ -17,7 +17,7 @@ import { rangeQuery, kqlQuery } from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; import { calculateThroughput } from '../../lib/helpers/calculate_throughput'; @@ -59,9 +59,7 @@ export async function getServiceTransactionGroups({ const { apmEventClient, config } = setup; const bucketSize = config.ui.transactionGroupBucketSize; - const field = getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ); + const field = getDurationFieldForTransactions(searchAggregatedTransactions); const response = await apmEventClient.search( 'get_service_transaction_groups', diff --git a/x-pack/plugins/apm/server/routes/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/routes/services/get_services/get_service_transaction_stats.ts index 3eaa8053b6709..9576c018c1c27 100644 --- a/x-pack/plugins/apm/server/routes/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/routes/services/get_services/get_service_transaction_stats.ts @@ -20,7 +20,7 @@ import { environmentQuery } from '../../../../common/utils/environment_query'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../../lib/helpers/transactions'; import { calculateThroughput } from '../../../lib/helpers/calculate_throughput'; @@ -56,9 +56,7 @@ export async function getServiceTransactionStats({ const metrics = { avg_duration: { avg: { - field: getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ), + field: getDurationFieldForTransactions(searchAggregatedTransactions), }, }, outcomes, diff --git a/x-pack/plugins/apm/server/routes/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts b/x-pack/plugins/apm/server/routes/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts index 95f2c6f400de9..744cd70e9eeb2 100644 --- a/x-pack/plugins/apm/server/routes/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/routes/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts @@ -19,7 +19,7 @@ import { environmentQuery } from '../../../../common/utils/environment_query'; import { getOffsetInMs } from '../../../../common/utils/get_offset_in_ms'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../../lib/helpers/transactions'; import { calculateThroughput } from '../../../lib/helpers/calculate_throughput'; @@ -61,9 +61,7 @@ export async function getServiceTransactionDetailedStatistics({ const metrics = { avg_duration: { avg: { - field: getTransactionDurationFieldForTransactions( - searchAggregatedTransactions - ), + field: getDurationFieldForTransactions(searchAggregatedTransactions), }, }, outcomes, diff --git a/x-pack/plugins/apm/server/routes/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/routes/transactions/get_latency_charts/index.ts index 5375da3606f18..5c9e4892866cb 100644 --- a/x-pack/plugins/apm/server/routes/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/routes/transactions/get_latency_charts/index.ts @@ -21,7 +21,7 @@ import { import { environmentQuery } from '../../../../common/utils/environment_query'; import { getDocumentTypeFilterForTransactions, - getTransactionDurationFieldForTransactions, + getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../../lib/helpers/transactions'; import { Setup } from '../../../lib/helpers/setup_request'; @@ -64,7 +64,7 @@ function searchLatency({ searchAggregatedTransactions, }); - const transactionDurationField = getTransactionDurationFieldForTransactions( + const transactionDurationField = getDurationFieldForTransactions( searchAggregatedTransactions ); diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts index bf607030c07d3..d4f0e350071bf 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts @@ -5,46 +5,63 @@ * 2.0. */ -import querystring from 'querystring'; -import url from 'url'; import expect from '@kbn/expect'; import { isEmpty, orderBy, uniq } from 'lodash'; +import { ServiceConnectionNode } from '../../../../plugins/apm/common/service_map'; +import { ApmApiError, SupertestReturnType } from '../../common/apm_api_supertest'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { PromiseReturnType } from '../../../../plugins/observability/typings/common'; import { FtrProviderContext } from '../../common/ftr_provider_context'; +type BackendResponse = SupertestReturnType<'GET /internal/apm/service-map/backend'>; +type ServiceNodeResponse = + SupertestReturnType<'GET /internal/apm/service-map/service/{serviceName}'>; +type ServiceMapResponse = SupertestReturnType<'GET /internal/apm/service-map'>; + export default function serviceMapsApiTests({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); - const supertestAsApmReadUserWithoutMlAccess = getService( - 'legacySupertestAsApmReadUserWithoutMlAccess' - ); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; - const start = encodeURIComponent(metadata.start); - const end = encodeURIComponent(metadata.end); registry.when('Service map with a basic license', { config: 'basic', archives: [] }, () => { it('is only be available to users with Platinum license (or higher)', async () => { - const response = await supertest.get( - `/internal/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` - ); - - expect(response.status).to.be(403); + try { + await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map`, + params: { + query: { + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); - expectSnapshot(response.body.message).toMatchInline( - `"In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data."` - ); + expect(true).to.be(false); + } catch (e) { + const err = e as ApmApiError; + expect(err.res.status).to.be(403); + expectSnapshot(err.res.body.message).toMatchInline( + `"In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data."` + ); + } }); }); registry.when('Service map without data', { config: 'trial', archives: [] }, () => { describe('/internal/apm/service-map', () => { it('returns an empty list', async () => { - const response = await supertest.get( - `/internal/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` - ); + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map`, + params: { + query: { + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); expect(response.status).to.be(200); expect(response.body.elements.length).to.be(0); @@ -52,63 +69,78 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); describe('/internal/apm/service-map/service/{serviceName}', () => { - it('returns an object with nulls', async () => { - const q = querystring.stringify({ - start: metadata.start, - end: metadata.end, - environment: 'ENVIRONMENT_ALL', + let response: ServiceNodeResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/service/{serviceName}`, + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + }, + }, }); - const response = await supertest.get(`/internal/apm/service-map/service/opbeans-node?${q}`); + }); + it('retuns status code 200', () => { expect(response.status).to.be(200); + }); - expectSnapshot(response.body).toMatchInline(` - Object { - "avgCpuUsage": null, - "avgErrorRate": null, - "avgMemoryUsage": null, - "transactionStats": Object { - "avgRequestsPerMinute": null, - "avgTransactionDuration": null, - }, - } - `); + it('returns an object with nulls', async () => { + [ + response.body.failedTransactionsRate?.value, + response.body.memoryUsage?.value, + response.body.cpuUsage?.value, + response.body.transactionStats?.latency?.value, + response.body.transactionStats?.throughput?.value, + ].forEach((value) => { + expect(value).to.be.eql(null); + }); }); }); describe('/internal/apm/service-map/backend', () => { - it('returns an object with nulls', async () => { - const q = querystring.stringify({ - backendName: 'postgres', - start: metadata.start, - end: metadata.end, - environment: 'ENVIRONMENT_ALL', + let response: BackendResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/backend`, + params: { + query: { + backendName: 'postgres', + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + }, + }, }); - const response = await supertest.get(`/internal/apm/service-map/backend?${q}`); + }); + it('retuns status code 200', () => { expect(response.status).to.be(200); + }); - expectSnapshot(response.body).toMatchInline(` - Object { - "avgErrorRate": null, - "transactionStats": Object { - "avgRequestsPerMinute": null, - "avgTransactionDuration": null, - }, - } - `); + it('returns undefined values', () => { + expect(response.body).to.eql({ transactionStats: {} }); }); }); }); registry.when('Service Map with data', { config: 'trial', archives: ['apm_8.0.0'] }, () => { describe('/internal/apm/service-map', () => { - let response: PromiseReturnType; - + let response: ServiceMapResponse; before(async () => { - response = await supertest.get( - `/internal/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` - ); + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map`, + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: metadata.start, + end: metadata.end, + }, + }, + }); }); it('returns service map elements', () => { @@ -126,17 +158,17 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) ).sort(); expectSnapshot(serviceNames).toMatchInline(` - Array [ - "auditbeat", - "opbeans-dotnet", - "opbeans-go", - "opbeans-java", - "opbeans-node", - "opbeans-python", - "opbeans-ruby", - "opbeans-rum", - ] - `); + Array [ + "auditbeat", + "opbeans-dotnet", + "opbeans-go", + "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-rum", + ] + `); const externalDestinations = uniq( elements @@ -145,115 +177,119 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) ).sort(); expectSnapshot(externalDestinations).toMatchInline(` - Array [ - ">elasticsearch", - ">postgresql", - ">redis", - ">sqlite", - ] - `); + Array [ + ">elasticsearch", + ">postgresql", + ">redis", + ">sqlite", + ] + `); }); describe('with ML data', () => { describe('with the default apm user', () => { before(async () => { - response = await supertest.get( - `/internal/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` - ); + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map`, + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: metadata.start, + end: metadata.end, + }, + }, + }); }); - it('returns service map elements with anomaly stats', () => { expect(response.status).to.be(200); const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + (el) => !isEmpty((el.data as ServiceConnectionNode).serviceAnomalyStats) ); - expect(dataWithAnomalies).not.to.be.empty(); - dataWithAnomalies.forEach(({ data }: any) => { expect( Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value)) ).to.not.empty(); }); }); - it('returns the correct anomaly stats', () => { const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + (el) => !isEmpty((el.data as ServiceConnectionNode).serviceAnomalyStats) ); - expect(dataWithAnomalies).not.to.be.empty(); - expectSnapshot(dataWithAnomalies.length).toMatchInline(`7`); expectSnapshot(orderBy(dataWithAnomalies, 'data.id').slice(0, 3)).toMatchInline(` - Array [ - Object { - "data": Object { - "agent.name": "dotnet", - "id": "opbeans-dotnet", - "service.environment": "production", - "service.name": "opbeans-dotnet", - "serviceAnomalyStats": Object { - "actualValue": 868025.86875, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-6117-high_mean_transaction_duration", - "serviceName": "opbeans-dotnet", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - "serviceAnomalyStats": Object { - "actualValue": 102786.319148936, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-testing-41e5-high_mean_transaction_duration", - "serviceName": "opbeans-go", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 175568.855769231, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-6117-high_mean_transaction_duration", - "serviceName": "opbeans-java", - "transactionType": "request", - }, - }, - }, - ] - `); + Array [ + Object { + "data": Object { + "agent.name": "dotnet", + "id": "opbeans-dotnet", + "service.environment": "production", + "service.name": "opbeans-dotnet", + "serviceAnomalyStats": Object { + "actualValue": 868025.86875, + "anomalyScore": 0, + "healthStatus": "healthy", + "jobId": "apm-production-6117-high_mean_transaction_duration", + "serviceName": "opbeans-dotnet", + "transactionType": "request", + }, + }, + }, + Object { + "data": Object { + "agent.name": "go", + "id": "opbeans-go", + "service.environment": "testing", + "service.name": "opbeans-go", + "serviceAnomalyStats": Object { + "actualValue": 102786.319148936, + "anomalyScore": 0, + "healthStatus": "healthy", + "jobId": "apm-testing-41e5-high_mean_transaction_duration", + "serviceName": "opbeans-go", + "transactionType": "request", + }, + }, + }, + Object { + "data": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + "serviceAnomalyStats": Object { + "actualValue": 175568.855769231, + "anomalyScore": 0, + "healthStatus": "healthy", + "jobId": "apm-production-6117-high_mean_transaction_duration", + "serviceName": "opbeans-java", + "transactionType": "request", + }, + }, + }, + ] + `); }); }); - describe('with a user that does not have access to ML', () => { before(async () => { - response = await supertestAsApmReadUserWithoutMlAccess.get( - `/internal/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` - ); + response = await apmApiClient.noMlAccessUser({ + endpoint: `GET /internal/apm/service-map`, + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: metadata.start, + end: metadata.end, + }, + }, + }); }); - it('returns service map elements without anomaly stats', () => { expect(response.status).to.be(200); - const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + (el) => !isEmpty((el.data as ServiceConnectionNode).serviceAnomalyStats) ); - expect(dataWithAnomalies).to.be.empty(); }); }); @@ -261,20 +297,25 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) describe('with a single service', () => { describe('when ENVIRONMENT_ALL is selected', () => { - it('returns service map elements', async () => { - response = await supertest.get( - url.format({ - pathname: '/internal/apm/service-map', + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map`, + params: { query: { environment: 'ENVIRONMENT_ALL', start: metadata.start, end: metadata.end, serviceName: 'opbeans-java', }, - }) - ); + }, + }); + }); + it('retuns status code 200', () => { expect(response.status).to.be(200); + }); + + it('returns some elements', () => { expect(response.body.elements.length).to.be.greaterThan(1); }); }); @@ -282,51 +323,79 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); describe('/internal/apm/service-map/service/{serviceName}', () => { - it('returns an object with data', async () => { - const q = querystring.stringify({ - start: metadata.start, - end: metadata.end, - environment: 'ENVIRONMENT_ALL', + let response: ServiceNodeResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/service/{serviceName}`, + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + }, + }, }); - const response = await supertest.get(`/internal/apm/service-map/service/opbeans-node?${q}`); + }); + it('retuns status code 200', () => { expect(response.status).to.be(200); + }); - expectSnapshot(response.body).toMatchInline(` - Object { - "avgCpuUsage": 0.240216666666667, - "avgErrorRate": 0, - "avgMemoryUsage": 0.202572668763642, - "transactionStats": Object { - "avgRequestsPerMinute": 5.2, - "avgTransactionDuration": 53906.6603773585, - }, - } - `); + it('returns some error rate', () => { + expect(response.body.failedTransactionsRate?.value).to.eql(0); + expect(response.body.failedTransactionsRate?.timeseries?.length).to.be.greaterThan(0); + }); + + it('returns some latency', () => { + expect(response.body.transactionStats?.latency?.value).to.be.greaterThan(0); + expect(response.body.transactionStats?.latency?.timeseries?.length).to.be.greaterThan(0); + }); + + it('returns some throughput', () => { + expect(response.body.transactionStats?.throughput?.value).to.be.greaterThan(0); + expect(response.body.transactionStats?.throughput?.timeseries?.length).to.be.greaterThan(0); + }); + + it('returns some cpu usage', () => { + expect(response.body.cpuUsage?.value).to.be.greaterThan(0); + expect(response.body.cpuUsage?.timeseries?.length).to.be.greaterThan(0); }); }); describe('/internal/apm/service-map/backend', () => { - it('returns an object with data', async () => { - const q = querystring.stringify({ - backendName: 'postgresql', - start: metadata.start, - end: metadata.end, - environment: 'ENVIRONMENT_ALL', + let response: BackendResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/backend`, + params: { + query: { + backendName: 'postgresql', + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + }, + }, }); - const response = await supertest.get(`/internal/apm/service-map/backend?${q}`); + }); + it('retuns status code 200', () => { expect(response.status).to.be(200); + }); - expectSnapshot(response.body).toMatchInline(` - Object { - "avgErrorRate": 0, - "transactionStats": Object { - "avgRequestsPerMinute": 82.9666666666667, - "avgTransactionDuration": 18307.583366814, - }, - } - `); + it('returns some error rate', () => { + expect(response.body.failedTransactionsRate?.value).to.eql(0); + expect(response.body.failedTransactionsRate?.timeseries?.length).to.be.greaterThan(0); + }); + + it('returns some latency', () => { + expect(response.body.transactionStats?.latency?.value).to.be.greaterThan(0); + expect(response.body.transactionStats?.latency?.timeseries?.length).to.be.greaterThan(0); + }); + + it('returns some throughput', () => { + expect(response.body.transactionStats?.throughput?.value).to.be.greaterThan(0); + expect(response.body.transactionStats?.throughput?.timeseries?.length).to.be.greaterThan(0); }); }); });