diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 4d6c0be9ff818..80b43a571af11 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -24,6 +24,7 @@ import { ServiceOverview } from './'; import { waitFor } from '@testing-library/dom'; import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { TimeRangeComparisonType } from '../../shared/time_comparison/get_time_range_comparison'; import { getCallApmApiSpy, getCreateCallApmApiSpy, @@ -54,6 +55,8 @@ function Wrapper({ children }: { children?: ReactNode }) { rangeFrom: 'now-15m', rangeTo: 'now', latencyAggregationType: LatencyAggregationType.avg, + comparisonType: TimeRangeComparisonType.DayBefore, + comparisonEnabled: true, }} > {children} @@ -94,7 +97,9 @@ describe('ServiceOverview', () => { totalTransactionGroups: 0, isAggregationAccurate: true, }, - 'GET /api/apm/services/{serviceName}/dependencies': [], + 'GET /api/apm/services/{serviceName}/dependencies': { + serviceDependencies: [], + }, 'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics': [], }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/get_columns.tsx new file mode 100644 index 0000000000000..5c01e379e5179 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/get_columns.tsx @@ -0,0 +1,188 @@ +/* + * 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 { EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values'; +import { + asMillisecondDuration, + asPercent, + asTransactionRate, +} from '../../../../../common/utils/formatters'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +import { px, unit } from '../../../../style/variables'; +import { AgentIcon } from '../../../shared/AgentIcon'; +import { SparkPlot } from '../../../shared/charts/spark_plot'; +import { ImpactBar } from '../../../shared/ImpactBar'; +import { ServiceOverviewLink } from '../../../shared/Links/apm/service_overview_link'; +import { SpanIcon } from '../../../shared/span_icon'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; + +type ServiceDependencies = APIReturnType<'GET /api/apm/services/{serviceName}/dependencies'>; + +export function getColumns({ + environment, + comparisonEnabled, +}: { + environment?: string; + comparisonEnabled?: boolean; +}): Array> { + return [ + { + field: 'name', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnBackend', + { + defaultMessage: 'Backend', + } + ), + render: (_, item) => { + return ( + + + {item.type === 'service' ? ( + + ) : ( + + )} + + + {item.type === 'service' ? ( + + {item.name} + + ) : ( + item.name + )} + + + } + /> + ); + }, + sortable: true, + }, + { + field: 'latencyValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnLatency', + { + defaultMessage: 'Latency (avg.)', + } + ), + width: px(unit * 10), + render: (_, item) => { + const previousPeriodLatencyTimeseries = + item.previousPeriodMetrics?.latency?.timeseries; + return ( + + ); + }, + sortable: true, + }, + { + field: 'throughputValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnThroughput', + { defaultMessage: 'Throughput' } + ), + width: px(unit * 10), + render: (_, item) => { + const previousPeriodThroughputTimeseries = + item.previousPeriodMetrics?.throughput?.timeseries; + return ( + + ); + }, + sortable: true, + }, + { + field: 'errorRateValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnErrorRate', + { + defaultMessage: 'Error rate', + } + ), + width: px(unit * 10), + render: (_, item) => { + const previousPeriodErrorRateTimeseries = + item.previousPeriodMetrics?.errorRate?.timeseries; + return ( + + ); + }, + sortable: true, + }, + { + field: 'impactValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnImpact', + { + defaultMessage: 'Impact', + } + ), + width: px(unit * 5), + render: (_, item) => { + const previousPeriodImpact = item.previousPeriodMetrics?.impact || 0; + return ( + + + + + {comparisonEnabled && ( + + + + )} + + ); + }, + sortable: true, + }, + ]; +} 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 4ff42b151dc8e..fdb20c3df7e7b 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 @@ -6,7 +6,6 @@ */ import { - EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, @@ -14,162 +13,42 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values'; -import { - asMillisecondDuration, - asPercent, - asTransactionRate, -} from '../../../../../common/utils/formatters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceDependencyItem } from '../../../../../server/lib/services/get_service_dependencies'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { px, unit } from '../../../../style/variables'; -import { AgentIcon } from '../../../shared/AgentIcon'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; -import { ImpactBar } from '../../../shared/ImpactBar'; import { ServiceMapLink } from '../../../shared/Links/apm/ServiceMapLink'; -import { ServiceOverviewLink } from '../../../shared/Links/apm/service_overview_link'; -import { SpanIcon } from '../../../shared/span_icon'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; +import { getColumns } from './get_columns'; + +type ServiceDependencies = APIReturnType<'GET /api/apm/services/{serviceName}/dependencies'>; interface Props { serviceName: string; } +const INITIAL_STATE = { + serviceDependencies: [], +} as ServiceDependencies; + export function ServiceOverviewDependenciesTable({ serviceName }: Props) { const { - urlParams: { start, end, environment }, + urlParams: { start, end, environment, comparisonType, comparisonEnabled }, } = useUrlParams(); - const columns: Array> = [ - { - field: 'name', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnBackend', - { - defaultMessage: 'Backend', - } - ), - render: (_, item) => { - return ( - - - {item.type === 'service' ? ( - - ) : ( - - )} - - - {item.type === 'service' ? ( - - {item.name} - - ) : ( - item.name - )} - - - } - /> - ); - }, - sortable: true, - }, - { - field: 'latencyValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnLatency', - { - defaultMessage: 'Latency (avg.)', - } - ), - width: px(unit * 10), - render: (_, { latency }) => { - return ( - - ); - }, - sortable: true, - }, - { - field: 'throughputValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnThroughput', - { defaultMessage: 'Throughput' } - ), - width: px(unit * 10), - render: (_, { throughput }) => { - return ( - - ); - }, - sortable: true, - }, - { - field: 'errorRateValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnErrorRate', - { - defaultMessage: 'Error rate', - } - ), - width: px(unit * 10), - render: (_, { errorRate }) => { - return ( - - ); - }, - sortable: true, - }, - { - field: 'impactValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnImpact', - { - defaultMessage: 'Impact', - } - ), - width: px(unit * 5), - render: (_, { impact }) => { - return ; - }, - sortable: true, - }, - ]; + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + comparisonEnabled, + }); - const { data, status } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end) { return; } - return callApmApi({ endpoint: 'GET /api/apm/services/{serviceName}/dependencies', params: { @@ -181,24 +60,29 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { end, environment, numBuckets: 20, + comparisonStart, + comparisonEnd, }, }, }); }, - [start, end, serviceName, environment] + [start, end, serviceName, environment, comparisonStart, comparisonEnd] ); - const serviceDependencies = data?.serviceDependencies ?? []; - // need top-level sortable fields for the managed table - const items = serviceDependencies.map((item) => ({ + const items = data.serviceDependencies.map((item) => ({ ...item, - errorRateValue: item.errorRate.value, - latencyValue: item.latency.value, - throughputValue: item.throughput.value, - impactValue: item.impact, + errorRateValue: item.currentPeriodMetrics.errorRate.value, + latencyValue: item.currentPeriodMetrics.latency.value, + throughputValue: item.currentPeriodMetrics.throughput.value, + impactValue: item.currentPeriodMetrics.impact, })); + const columns = getColumns({ + environment, + comparisonEnabled, + }); + return ( diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts index db491012c986b..d65af67125c22 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts @@ -8,7 +8,11 @@ import { isEqual, keyBy, mapValues } from 'lodash'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { pickKeys } from '../../../../common/utils/pick_keys'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { + AgentName, + ElasticAgentName, + OpenTelemetryAgentName, +} from '../../../../typings/es_schemas/ui/fields/agent'; import { AGENT_NAME, EVENT_OUTCOME, @@ -26,6 +30,27 @@ import { joinByKey } from '../../../../common/utils/join_by_key'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { withApmSpan } from '../../../utils/with_apm_span'; +export type DestinationMap = Record< + string, + { + id: + | { 'span.destination.service.resource': string } + | { service?: { name: string; environment: string; agentName: string } }; + span: { + type?: string; + subtype?: string; + destination: { service: { resource?: string } }; + }; + service?: { + name: string; + environment: string; + }; + agent?: { + name: ElasticAgentName | OpenTelemetryAgentName; + }; + } +>; + export const getDestinationMap = ({ setup, serviceName, @@ -34,7 +59,7 @@ export const getDestinationMap = ({ setup: Setup & SetupTimeRange; serviceName: string; environment?: string; -}) => { +}): Promise => { return withApmSpan('get_service_destination_map', async () => { const { start, end, apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts index c8642c6272b5f..f9a4844243f15 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts @@ -17,7 +17,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { EventOutcome } from '../../../../common/event_outcome'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { Setup } from '../../helpers/setup_request'; import { withApmSpan } from '../../../utils/with_apm_span'; export const getMetrics = ({ @@ -25,14 +25,18 @@ export const getMetrics = ({ serviceName, environment, numBuckets, + start, + end, }: { - setup: Setup & SetupTimeRange; + setup: Setup; serviceName: string; environment?: string; numBuckets: number; + start: number; + end: number; }) => { return withApmSpan('get_service_destination_metrics', async () => { - const { start, end, apmEventClient } = setup; + const { apmEventClient } = setup; const response = await apmEventClient.search({ apm: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.test.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.test.ts new file mode 100644 index 0000000000000..c8e287dadf06a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.test.ts @@ -0,0 +1,815 @@ +/* + * 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 { PromiseReturnType } from '../../../../../observability/typings/common'; +import { getDestinationMap } from './get_destination_map'; +import { getMetrics } from './get_metrics'; +import { + getMetricsWithDestinationIds, + joinMetricsByDestinationId, + calculateMetricValues, + calculateDestinationMetrics, + offsetPreviousMetrics, + getCalculateImpact, +} from './helpers'; + +const destinationMap = { + postgresql: { + id: { 'span.destination.service.resource': 'postgresql' }, + span: { + type: 'db', + subtype: 'postgresql', + destination: { service: { resource: 'postgresql' } }, + }, + }, + 'opbeans:3000': { + id: { + service: { + name: 'opbeans-go', + environment: 'testing', + agentName: 'go', + }, + }, + span: { + type: 'external', + subtype: 'http', + destination: { service: { resource: 'opbeans:3000' } }, + }, + service: { name: 'opbeans-go', environment: 'testing' }, + agent: { name: 'go' }, + }, +} as PromiseReturnType; + +const currentPeriodMetrics = [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 184, latency_sum: 284666, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 12, latency_sum: 18830, error_count: 0 }, + { x: 1618858440000, count: 8, latency_sum: 9847, error_count: 0 }, + ], + }, + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { x: 1618858440000, count: 2, latency_sum: 17893, error_count: 0 }, + ], + }, +]; +describe('service dependencies helpers', () => { + describe('getMetricsWithDestinationIds', () => { + it('merges current and previous metrics based on destination', () => { + const previousPeriodMetrics = [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 187, latency_sum: 465185, error_count: 0 }, + timeseries: [ + { x: 1618771980000, count: 16, latency_sum: 31378, error_count: 0 }, + { x: 1618772040000, count: 4, latency_sum: 5774, error_count: 0 }, + ], + }, + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 16, latency_sum: 6060038, error_count: 0 }, + timeseries: [ + { x: 1618771980000, count: 2, latency_sum: 287255, error_count: 0 }, + { x: 1618772040000, count: 1, latency_sum: 27661, error_count: 0 }, + ], + }, + ]; + + expect( + getMetricsWithDestinationIds({ + destinationMap, + currentPeriodMetrics, + previousPeriodMetrics, + }) + ).toEqual([ + { + id: { 'span.destination.service.resource': 'postgresql' }, + metrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 184, latency_sum: 284666, error_count: 0 }, + timeseries: [ + { + x: 1618858380000, + count: 12, + latency_sum: 18830, + error_count: 0, + }, + { + x: 1618858440000, + count: 8, + latency_sum: 9847, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 187, latency_sum: 465185, error_count: 0 }, + timeseries: [ + { + x: 1618771980000, + count: 16, + latency_sum: 31378, + error_count: 0, + }, + { + x: 1618772040000, + count: 4, + latency_sum: 5774, + error_count: 0, + }, + ], + }, + ], + span: { + destination: { service: { resource: 'postgresql' } }, + type: 'db', + subtype: 'postgresql', + }, + }, + { + id: { + service: { + name: 'opbeans-go', + environment: 'testing', + agentName: 'go', + }, + }, + metrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { + x: 1618858440000, + count: 2, + latency_sum: 17893, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 16, latency_sum: 6060038, error_count: 0 }, + timeseries: [ + { + x: 1618771980000, + count: 2, + latency_sum: 287255, + error_count: 0, + }, + { + x: 1618772040000, + count: 1, + latency_sum: 27661, + error_count: 0, + }, + ], + }, + ], + span: { + destination: { service: { resource: 'opbeans:3000' } }, + type: 'external', + subtype: 'http', + }, + service: { name: 'opbeans-go', environment: 'testing' }, + agent: { name: 'go' }, + }, + ]); + }); + + it('returns only current metrics when destination is not found on previous metrics', () => { + const previousPeriodMetrics = [ + { + span: { destination: { service: { resource: 'foo' } } }, + value: { count: 187, latency_sum: 465185, error_count: 0 }, + timeseries: [ + { x: 1618771980000, count: 16, latency_sum: 31378, error_count: 0 }, + { x: 1618772040000, count: 4, latency_sum: 5774, error_count: 0 }, + ], + }, + { + span: { destination: { service: { resource: 'bar' } } }, + value: { count: 16, latency_sum: 6060038, error_count: 0 }, + timeseries: [ + { x: 1618771980000, count: 2, latency_sum: 287255, error_count: 0 }, + { x: 1618772040000, count: 1, latency_sum: 27661, error_count: 0 }, + ], + }, + ]; + expect( + getMetricsWithDestinationIds({ + destinationMap, + currentPeriodMetrics, + previousPeriodMetrics, + }) + ).toEqual([ + { + id: { 'span.destination.service.resource': 'postgresql' }, + metrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 184, latency_sum: 284666, error_count: 0 }, + timeseries: [ + { + x: 1618858380000, + count: 12, + latency_sum: 18830, + error_count: 0, + }, + { + x: 1618858440000, + count: 8, + latency_sum: 9847, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [], + span: { + destination: { service: { resource: 'postgresql' } }, + type: 'db', + subtype: 'postgresql', + }, + }, + { + id: { + service: { + name: 'opbeans-go', + environment: 'testing', + agentName: 'go', + }, + }, + metrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { + x: 1618858440000, + count: 2, + latency_sum: 17893, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [], + span: { + destination: { service: { resource: 'opbeans:3000' } }, + type: 'external', + subtype: 'http', + }, + service: { name: 'opbeans-go', environment: 'testing' }, + agent: { name: 'go' }, + }, + ]); + }); + + it('returns empty array when current metric is empty', () => { + const previousPeriodMetrics = [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 187, latency_sum: 465185, error_count: 0 }, + timeseries: [ + { x: 1618771980000, count: 16, latency_sum: 31378, error_count: 0 }, + { x: 1618772040000, count: 4, latency_sum: 5774, error_count: 0 }, + ], + }, + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 16, latency_sum: 6060038, error_count: 0 }, + timeseries: [ + { x: 1618771980000, count: 2, latency_sum: 287255, error_count: 0 }, + { x: 1618772040000, count: 1, latency_sum: 27661, error_count: 0 }, + ], + }, + ]; + + expect( + getMetricsWithDestinationIds({ + destinationMap, + currentPeriodMetrics: [] as PromiseReturnType, + previousPeriodMetrics, + }) + ).toEqual([]); + }); + + it('returns empty previous metric when previous metric is empty', () => { + expect( + getMetricsWithDestinationIds({ + destinationMap, + currentPeriodMetrics, + previousPeriodMetrics: [] as PromiseReturnType, + }) + ).toEqual([ + { + id: { 'span.destination.service.resource': 'postgresql' }, + metrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 184, latency_sum: 284666, error_count: 0 }, + timeseries: [ + { + x: 1618858380000, + count: 12, + latency_sum: 18830, + error_count: 0, + }, + { + x: 1618858440000, + count: 8, + latency_sum: 9847, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [], + span: { + destination: { service: { resource: 'postgresql' } }, + type: 'db', + subtype: 'postgresql', + }, + }, + { + id: { + service: { + name: 'opbeans-go', + environment: 'testing', + agentName: 'go', + }, + }, + metrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { + x: 1618858440000, + count: 2, + latency_sum: 17893, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [], + span: { + destination: { service: { resource: 'opbeans:3000' } }, + type: 'external', + subtype: 'http', + }, + service: { name: 'opbeans-go', environment: 'testing' }, + agent: { name: 'go' }, + }, + ]); + }); + }); + + describe('joinMetricsByDestinationId', () => { + it('returns empty when receives an empty metrics', () => { + expect( + joinMetricsByDestinationId( + [] as ReturnType + ) + ).toEqual([]); + }); + it('returns metrics joinned by destination', () => { + const metricsWithDestinationIds = [ + { + id: { 'span.destination.service.resource': 'postgresql' }, + metrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 184, latency_sum: 284666, error_count: 0 }, + timeseries: [ + { + x: 1618858380000, + count: 12, + latency_sum: 18830, + error_count: 0, + }, + { + x: 1618858440000, + count: 8, + latency_sum: 9847, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 187, latency_sum: 465185, error_count: 0 }, + timeseries: [ + { + x: 1618771980000, + count: 16, + latency_sum: 31378, + error_count: 0, + }, + { + x: 1618772040000, + count: 4, + latency_sum: 5774, + error_count: 0, + }, + ], + }, + ], + span: { + destination: { service: { resource: 'postgresql' } }, + type: 'db', + subtype: 'postgresql', + }, + }, + { + id: { + service: { + name: 'opbeans-go', + environment: 'testing', + agentName: 'go', + }, + }, + metrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { + x: 1618858440000, + count: 2, + latency_sum: 17893, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 16, latency_sum: 6060038, error_count: 0 }, + timeseries: [ + { + x: 1618771980000, + count: 2, + latency_sum: 287255, + error_count: 0, + }, + { + x: 1618772040000, + count: 1, + latency_sum: 27661, + error_count: 0, + }, + ], + }, + ], + span: { + destination: { service: { resource: 'opbeans:3000' } }, + type: 'external', + subtype: 'http', + }, + service: { name: 'opbeans-go', environment: 'testing' }, + agent: { name: 'go' }, + }, + ] as ReturnType; + expect(joinMetricsByDestinationId(metricsWithDestinationIds)).toEqual([ + { + id: { 'span.destination.service.resource': 'postgresql' }, + metrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 184, latency_sum: 284666, error_count: 0 }, + timeseries: [ + { + x: 1618858380000, + count: 12, + latency_sum: 18830, + error_count: 0, + }, + { + x: 1618858440000, + count: 8, + latency_sum: 9847, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [ + { + span: { destination: { service: { resource: 'postgresql' } } }, + value: { count: 187, latency_sum: 465185, error_count: 0 }, + timeseries: [ + { + x: 1618771980000, + count: 16, + latency_sum: 31378, + error_count: 0, + }, + { + x: 1618772040000, + count: 4, + latency_sum: 5774, + error_count: 0, + }, + ], + }, + ], + span: { + destination: { service: { resource: 'postgresql' } }, + type: 'db', + subtype: 'postgresql', + }, + }, + { + id: { + service: { + name: 'opbeans-go', + environment: 'testing', + agentName: 'go', + }, + }, + metrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { + x: 1618858440000, + count: 2, + latency_sum: 17893, + error_count: 0, + }, + ], + }, + ], + previousMetrics: [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 16, latency_sum: 6060038, error_count: 0 }, + timeseries: [ + { + x: 1618771980000, + count: 2, + latency_sum: 287255, + error_count: 0, + }, + { + x: 1618772040000, + count: 1, + latency_sum: 27661, + error_count: 0, + }, + ], + }, + ], + span: { + destination: { service: { resource: 'opbeans:3000' } }, + type: 'external', + subtype: 'http', + }, + service: { name: 'opbeans-go', environment: 'testing' }, + agent: { name: 'go' }, + }, + ]); + }); + }); + describe('calculateMetricValues', () => { + it('returns default value when empty', () => { + expect( + calculateMetricValues( + [] as ReturnType[0]['metrics'] + ) + ).toEqual({ + value: { count: 0, latency_sum: 0, error_count: 0 }, + timeseries: [], + }); + }); + it('calculate metrics', () => { + const metrics = [ + { + span: { destination: { service: { resource: 'opbeans:3000' } } }, + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { + x: 1618858440000, + count: 2, + latency_sum: 17893, + error_count: 0, + }, + ], + }, + ]; + expect(calculateMetricValues(metrics)).toEqual({ + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { x: 1618858440000, count: 2, latency_sum: 17893, error_count: 0 }, + ], + }); + }); + }); + + describe('calculateDestinationMetrics', () => { + it('return empty timeseries', () => { + expect( + calculateDestinationMetrics({ + mergedMetrics: { + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [], + }, + start: new Date('2021-04-19T22:04:12.205Z').valueOf(), + end: new Date('2021-04-19T22:09:17.798Z').valueOf(), + }) + ).toEqual({ + errorRate: { timeseries: [], value: 0 }, + latency: { timeseries: [], value: 305487.05555555556 }, + throughput: { timeseries: [], value: 3.534112365139254 }, + }); + }); + + it('returns metrics with timeseries', () => { + expect( + calculateDestinationMetrics({ + mergedMetrics: { + value: { count: 18, latency_sum: 5498767, error_count: 0 }, + timeseries: [ + { x: 1618858380000, count: 0, latency_sum: 0, error_count: 0 }, + { + x: 1618858440000, + count: 2, + latency_sum: 17893, + error_count: 0, + }, + ], + }, + start: new Date('2021-04-19T22:04:12.205Z').valueOf(), + end: new Date('2021-04-19T22:09:17.798Z').valueOf(), + }) + ).toEqual({ + errorRate: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 0 }, + ], + value: 0, + }, + latency: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 8946.5 }, + ], + value: 305487.05555555556, + }, + throughput: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 0.3926791516821393 }, + ], + value: 3.534112365139254, + }, + }); + }); + }); + + describe('offsetPreviousMetrics', () => { + it('return empty object when no previous period is informed', () => { + expect( + offsetPreviousMetrics({ + currentDestinationMetrics: { + errorRate: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 0 }, + ], + value: 0, + }, + latency: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 8946.5 }, + ], + value: 305487.05555555556, + }, + throughput: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 0.3926791516821393 }, + ], + value: 3.534112365139254, + }, + }, + }) + ).toEqual({}); + }); + it('offsets previous metrics timeseries', () => { + const currentDestinationMetrics = { + errorRate: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 0 }, + ], + value: 0, + }, + latency: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 8946.5 }, + ], + value: 305487.05555555556, + }, + throughput: { + timeseries: [ + { x: 1618858380000, y: null }, + { x: 1618858440000, y: 0.3926791516821393 }, + ], + value: 3.534112365139254, + }, + }; + const previousDestinationMetrics = { + errorRate: { + timeseries: [ + { x: 1618771980000, y: 1 }, + { x: 1618772040000, y: 2 }, + ], + value: 0, + }, + latency: { + timeseries: [ + { x: 1618771980000, y: 0 }, + { x: 1618772040000, y: 5 }, + ], + value: 305487.05555555556, + }, + throughput: { + timeseries: [ + { x: 1618771980000, y: 4 }, + { x: 1618772040000, y: 7 }, + ], + value: 3.534112365139254, + }, + }; + + const offsetData = offsetPreviousMetrics({ + currentDestinationMetrics, + previousDestinationMetrics, + }); + expect(offsetData.latency?.timeseries.map(({ x }) => x)).toEqual( + currentDestinationMetrics.latency.timeseries.map(({ x }) => x) + ); + expect(offsetData.throughput?.timeseries.map(({ x }) => x)).toEqual( + currentDestinationMetrics.throughput.timeseries.map(({ x }) => x) + ); + expect(offsetData.errorRate?.timeseries.map(({ x }) => x)).toEqual( + currentDestinationMetrics.errorRate.timeseries.map(({ x }) => x) + ); + }); + }); + + describe('getCalculateImpact', () => { + it('returns a function', () => { + const latencySums = [1, 2, 3, 4]; + const calculateImpact = getCalculateImpact(latencySums); + expect(typeof calculateImpact === 'function').toBeTruthy(); + }); + it('returns 0 when values are null', () => { + const latencySums = [1, 2, 3, 4]; + const calculateImpact = getCalculateImpact(latencySums); + expect( + calculateImpact({ latencyValue: null, throughputValue: null }) + ).toEqual(0); + }); + it('returns correct impact', () => { + const latencySums = [1, 2, 3, 4]; + const calculateImpact = getCalculateImpact(latencySums); + expect(calculateImpact({ latencyValue: 3, throughputValue: 1 })).toEqual( + 66.66666666666666 + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.ts new file mode 100644 index 0000000000000..44f0b77bede8c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.ts @@ -0,0 +1,212 @@ +/* + * 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 { merge } from 'lodash'; +import { ValuesType } from 'utility-types'; +import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; +import { joinByKey } from '../../../../common/utils/join_by_key'; +import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; +import { maybe } from '../../../../common/utils/maybe'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { DestinationMap } from './get_destination_map'; +import { getMetrics } from './get_metrics'; +import { calculateThroughput } from '../../helpers/calculate_throughput'; +import { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate'; + +export function getMetricsWithDestinationIds({ + destinationMap, + currentPeriodMetrics, + previousPeriodMetrics, +}: { + destinationMap: DestinationMap; + currentPeriodMetrics: PromiseReturnType; + previousPeriodMetrics: PromiseReturnType; +}) { + return currentPeriodMetrics.map((metricItem) => { + const spanDestination = metricItem.span.destination.service.resource; + + const previousMetrics = previousPeriodMetrics.find( + (previousMetric) => + previousMetric.span.destination.service.resource === spanDestination + ); + + const destination = maybe(destinationMap[spanDestination]); + const id = destination?.id || { + [SPAN_DESTINATION_SERVICE_RESOURCE]: spanDestination, + }; + + return merge( + { + id, + metrics: [metricItem], + previousMetrics: previousMetrics ? [previousMetrics] : [], + span: { destination: { service: { resource: spanDestination } } }, + }, + destination + ); + }, []); +} + +export function joinMetricsByDestinationId( + metricsWithDestinationIds: ReturnType +) { + return joinByKey(metricsWithDestinationIds, 'id', (a, b) => { + const { metrics: metricsA, ...itemA } = a; + const { metrics: metricsB, ...itemB } = b; + + return merge({}, itemA, itemB, { + metrics: metricsA.concat(metricsB), + }); + }); +} + +export function calculateMetricValues( + metrics: + | ReturnType[0]['metrics'] + | ReturnType[0]['previousMetrics'] +) { + return metrics.reduce, 'span'>>( + (prev, current) => { + return { + value: { + count: prev.value.count + current.value.count, + latency_sum: prev.value.latency_sum + current.value.latency_sum, + error_count: prev.value.error_count + current.value.error_count, + }, + timeseries: joinByKey( + [...prev.timeseries, ...current.timeseries], + 'x', + (a, b) => ({ + x: a.x, + count: a.count + b.count, + latency_sum: a.latency_sum + b.latency_sum, + error_count: a.error_count + b.error_count, + }) + ), + }; + }, + { + value: { count: 0, latency_sum: 0, error_count: 0 }, + timeseries: [], + } + ); +} + +export function calculateDestinationMetrics({ + mergedMetrics, + start, + end, +}: { + mergedMetrics: ReturnType; + start: number; + end: number; +}) { + return { + latency: { + value: + mergedMetrics.value.count > 0 + ? mergedMetrics.value.latency_sum / mergedMetrics.value.count + : null, + timeseries: mergedMetrics.timeseries.map((point) => ({ + x: point.x, + y: point.count > 0 ? point.latency_sum / point.count : null, + })), + }, + throughput: { + value: + mergedMetrics.value.count > 0 + ? calculateThroughput({ + start, + end, + value: mergedMetrics.value.count, + }) + : null, + timeseries: mergedMetrics.timeseries.map((point) => ({ + x: point.x, + y: + point.count > 0 + ? calculateThroughput({ + start, + end, + value: point.count, + }) + : null, + })), + }, + errorRate: { + value: + mergedMetrics.value.count > 0 + ? (mergedMetrics.value.error_count ?? 0) / mergedMetrics.value.count + : null, + timeseries: mergedMetrics.timeseries.map((point) => ({ + x: point.x, + y: point.count > 0 ? (point.error_count ?? 0) / point.count : null, + })), + }, + }; +} + +export function offsetPreviousMetrics({ + currentDestinationMetrics, + previousDestinationMetrics, +}: { + currentDestinationMetrics: ReturnType; + previousDestinationMetrics?: ReturnType; +}) { + return previousDestinationMetrics + ? { + latency: { + ...previousDestinationMetrics.latency, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: + currentDestinationMetrics.latency.timeseries, + previousPeriodTimeseries: + previousDestinationMetrics.latency.timeseries, + }), + }, + throughput: { + ...previousDestinationMetrics.throughput, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: + currentDestinationMetrics.throughput.timeseries, + previousPeriodTimeseries: + previousDestinationMetrics.throughput.timeseries, + }), + }, + errorRate: { + ...previousDestinationMetrics.errorRate, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: + currentDestinationMetrics.errorRate.timeseries, + previousPeriodTimeseries: + previousDestinationMetrics.errorRate.timeseries, + }), + }, + } + : {}; +} + +export function getCalculateImpact(latencySums: number[]) { + const minLatencySum = Math.min(...latencySums); + const maxLatencySum = Math.max(...latencySums); + + return ({ + latencyValue, + throughputValue, + }: { + latencyValue: number | null; + throughputValue: number | null; + }) => { + const previousImpact = + isFiniteNumber(latencyValue) && isFiniteNumber(throughputValue) + ? ((latencyValue * throughputValue - minLatencySum) / + (maxLatencySum - minLatencySum)) * + 100 + : 0; + return previousImpact; + }; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts index 724b5278d7edf..f80b5b3cb51e8 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts @@ -5,181 +5,129 @@ * 2.0. */ -import { ValuesType } from 'utility-types'; -import { merge } from 'lodash'; -import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; -import { maybe } from '../../../../common/utils/maybe'; import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { joinByKey } from '../../../../common/utils/join_by_key'; +import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getMetrics } from './get_metrics'; import { getDestinationMap } from './get_destination_map'; -import { calculateThroughput } from '../../helpers/calculate_throughput'; -import { withApmSpan } from '../../../utils/with_apm_span'; +import { getMetrics } from './get_metrics'; +import { + calculateDestinationMetrics, + getCalculateImpact, + getMetricsWithDestinationIds, + joinMetricsByDestinationId, + calculateMetricValues, + offsetPreviousMetrics, +} from './helpers'; + +async function getServiceMetricsAndDestinationMap({ + setup, + serviceName, + environment, + numBuckets, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + environment?: string; + numBuckets: number; +}) { + const { start, end } = setup; + const [metrics, destinationMap] = await Promise.all([ + getMetrics({ + setup, + serviceName, + environment, + numBuckets, + start, + end, + }), + getDestinationMap({ + setup, + serviceName, + environment, + }), + ]); + return { metrics, destinationMap }; +} -export type ServiceDependencyItem = { - name: string; - latency: { - value: number | null; - timeseries: Array<{ x: number; y: number | null }>; - }; - throughput: { - value: number | null; - timeseries: Array<{ x: number; y: number | null }>; - }; - errorRate: { - value: number | null; - timeseries: Array<{ x: number; y: number | null }>; - }; - impact: number; -} & ( - | { - type: 'service'; - serviceName: string; - agentName: AgentName; - environment?: string; - } - | { type: 'external'; spanType?: string; spanSubtype?: string } -); - -export function getServiceDependencies({ +export async function getServiceDependenciesPerPeriod({ setup, serviceName, environment, numBuckets, + comparisonStart, + comparisonEnd, }: { serviceName: string; setup: Setup & SetupTimeRange; environment?: string; numBuckets: number; -}): Promise { + comparisonStart?: number; + comparisonEnd?: number; +}) { return withApmSpan('get_service_dependencies', async () => { const { start, end } = setup; - const [allMetrics, destinationMap] = await Promise.all([ - getMetrics({ - setup, - serviceName, - environment, - numBuckets, - }), - getDestinationMap({ - setup, - serviceName, - environment, - }), - ]); - - const metricsWithDestinationIds = allMetrics.map((metricItem) => { - const spanDestination = metricItem.span.destination.service.resource; - - const destination = maybe(destinationMap[spanDestination]); - const id = destination?.id || { - [SPAN_DESTINATION_SERVICE_RESOURCE]: spanDestination, - }; - return merge( - { - id, - metrics: [metricItem], - span: { - destination: { - service: { - resource: spanDestination, - }, - }, - }, - }, - destination - ); - }, []); - - const metricsJoinedByDestinationId = joinByKey( - metricsWithDestinationIds, - 'id', - (a, b) => { - const { metrics: metricsA, ...itemA } = a; - const { metrics: metricsB, ...itemB } = b; - - return merge({}, itemA, itemB, { metrics: metricsA.concat(metricsB) }); - } + const currentPeriodPromise = getServiceMetricsAndDestinationMap({ + setup, + serviceName, + environment, + numBuckets, + }); + + const previousPeriodPromise = + comparisonStart && comparisonEnd + ? getMetrics({ + setup, + serviceName, + environment, + numBuckets, + start: comparisonStart, + end: comparisonEnd, + }) + : []; + + const [ + currentPeriodMetricsAndDestinationMap, + previousPeriodMetrics, + ] = await Promise.all([currentPeriodPromise, previousPeriodPromise]); + + const metricsWithDestinationIds = getMetricsWithDestinationIds({ + destinationMap: currentPeriodMetricsAndDestinationMap.destinationMap, + currentPeriodMetrics: currentPeriodMetricsAndDestinationMap.metrics, + previousPeriodMetrics, + }); + + const metricsJoinedByDestinationId = joinMetricsByDestinationId( + metricsWithDestinationIds ); const metricsByResolvedAddress = metricsJoinedByDestinationId.map( (item) => { - const mergedMetrics = item.metrics.reduce< - Omit, 'span'> - >( - (prev, current) => { - return { - value: { - count: prev.value.count + current.value.count, - latency_sum: prev.value.latency_sum + current.value.latency_sum, - error_count: prev.value.error_count + current.value.error_count, - }, - timeseries: joinByKey( - [...prev.timeseries, ...current.timeseries], - 'x', - (a, b) => ({ - x: a.x, - count: a.count + b.count, - latency_sum: a.latency_sum + b.latency_sum, - error_count: a.error_count + b.error_count, - }) - ), - }; - }, - { - value: { - count: 0, - latency_sum: 0, - error_count: 0, - }, - timeseries: [], - } + const { metrics, previousMetrics } = item; + const mergedMetrics = calculateMetricValues(metrics); + const previousPeriodMergedMetrics = calculateMetricValues( + previousMetrics ); - const destMetrics = { - latency: { - value: - mergedMetrics.value.count > 0 - ? mergedMetrics.value.latency_sum / mergedMetrics.value.count - : null, - timeseries: mergedMetrics.timeseries.map((point) => ({ - x: point.x, - y: point.count > 0 ? point.latency_sum / point.count : null, - })), - }, - throughput: { - value: - mergedMetrics.value.count > 0 - ? calculateThroughput({ - start, - end, - value: mergedMetrics.value.count, - }) - : null, - timeseries: mergedMetrics.timeseries.map((point) => ({ - x: point.x, - y: - point.count > 0 - ? calculateThroughput({ start, end, value: point.count }) - : null, - })), - }, - errorRate: { - value: - mergedMetrics.value.count > 0 - ? (mergedMetrics.value.error_count ?? 0) / - mergedMetrics.value.count - : null, - timeseries: mergedMetrics.timeseries.map((point) => ({ - x: point.x, - y: - point.count > 0 ? (point.error_count ?? 0) / point.count : null, - })), - }, - }; + const currentDestinationMetrics = calculateDestinationMetrics({ + mergedMetrics, + start, + end, + }); + + const previousDestinationMetrics = + comparisonStart && comparisonEnd + ? calculateDestinationMetrics({ + mergedMetrics: previousPeriodMergedMetrics, + start: comparisonStart, + end: comparisonEnd, + }) + : undefined; + + const offsetPreviousDestinationMetrics = offsetPreviousMetrics({ + currentDestinationMetrics, + previousDestinationMetrics, + }); if (item.service) { return { @@ -189,7 +137,8 @@ export function getServiceDependencies({ environment: item.service.environment, // agent.name should always be there, type returned from joinByKey is too pessimistic agentName: item.agent!.name, - ...destMetrics, + currentPeriodMetrics: currentDestinationMetrics, + previousPeriodMetrics: offsetPreviousDestinationMetrics, }; } @@ -198,33 +147,53 @@ export function getServiceDependencies({ type: 'external' as const, spanType: item.span.type, spanSubtype: item.span.subtype, - ...destMetrics, + currentPeriodMetrics: currentDestinationMetrics, + previousPeriodMetrics: offsetPreviousDestinationMetrics, }; } ); - const latencySums = metricsByResolvedAddress - .map( - (metric) => (metric.latency.value ?? 0) * (metric.throughput.value ?? 0) - ) - .filter(isFiniteNumber); - - const minLatencySum = Math.min(...latencySums); - const maxLatencySum = Math.max(...latencySums); + const calculateCurrentImpact = getCalculateImpact( + metricsByResolvedAddress + .map( + (metric) => + (metric.currentPeriodMetrics.latency.value ?? 0) * + (metric.currentPeriodMetrics.throughput.value ?? 0) + ) + .filter(isFiniteNumber) + ); - return metricsByResolvedAddress.map((metric) => { - const impact = - isFiniteNumber(metric.latency.value) && - isFiniteNumber(metric.throughput.value) - ? ((metric.latency.value * metric.throughput.value - minLatencySum) / - (maxLatencySum - minLatencySum)) * - 100 - : 0; + const calculatePreviousImpact = getCalculateImpact( + metricsByResolvedAddress + .map( + (metric) => + (metric.previousPeriodMetrics.latency?.value ?? 0) * + (metric.previousPeriodMetrics.throughput?.value ?? 0) + ) + .filter(isFiniteNumber) + ); + const serviceDependencies = metricsByResolvedAddress.map((metric) => { return { ...metric, - impact, + currentPeriodMetrics: { + ...metric.currentPeriodMetrics, + impact: calculateCurrentImpact({ + latencyValue: metric.currentPeriodMetrics.latency.value, + throughputValue: metric.currentPeriodMetrics.throughput.value, + }), + }, + previousPeriodMetrics: { + ...metric.previousPeriodMetrics, + impact: calculatePreviousImpact({ + latencyValue: metric.previousPeriodMetrics.latency?.value ?? 0, + throughputValue: + metric.previousPeriodMetrics.throughput?.value ?? 0, + }), + }, }; }); + + return { serviceDependencies }; }); } diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index a27c7d5ba38d2..7cfb41e497887 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,8 +16,8 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; +import { getServiceDependenciesPerPeriod } from '../lib/services/get_service_dependencies'; import { getServiceAlerts } from '../lib/services/get_service_alerts'; -import { getServiceDependencies } from '../lib/services/get_service_dependencies'; import { getServiceInstanceMetadataDetails } from '../lib/services/get_service_instance_metadata_details'; import { getServiceErrorGroupPeriods } from '../lib/services/get_service_error_groups/get_service_error_group_detailed_statistics'; import { getServiceErrorGroupMainStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_main_statistics'; @@ -599,6 +599,7 @@ export const serviceDependenciesRoute = createApmServerRoute({ t.type({ numBuckets: toNumberRt, }), + comparisonRangeRt, environmentRt, rangeRt, ]), @@ -608,18 +609,23 @@ export const serviceDependenciesRoute = createApmServerRoute({ }, handler: async (resources) => { const setup = await setupRequest(resources); - const { params } = resources; - const { serviceName } = params.path; - const { environment, numBuckets } = params.query; - const serviceDependencies = await getServiceDependencies({ + const { serviceName } = resources.params.path; + const { + environment, + numBuckets, + comparisonStart, + comparisonEnd, + } = resources.params.query; + + return getServiceDependenciesPerPeriod({ serviceName, environment, setup, numBuckets, + comparisonStart, + comparisonEnd, }); - - return { serviceDependencies }; }, }); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/__snapshots__/index.snap b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/__snapshots__/index.snap new file mode 100644 index 0000000000000..dcd6e84dcfa0f --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/__snapshots__/index.snap @@ -0,0 +1,860 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM API tests basic apm_8.0.0 Service overview dependencies when data is loaded returns at least one item 1`] = ` +Object { + "currentPeriodMetrics": Object { + "errorRate": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 0, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 0, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], + "value": 0, + }, + "impact": 2.37724265214801, + "latency": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 1344.68, + }, + Object { + "x": 1607436780000, + "y": 731.439024390244, + }, + Object { + "x": 1607436840000, + "y": 945.684210526316, + }, + Object { + "x": 1607436900000, + "y": 1187.1914893617, + }, + Object { + "x": 1607436960000, + "y": 884.705882352941, + }, + Object { + "x": 1607437020000, + "y": 839.604651162791, + }, + Object { + "x": 1607437080000, + "y": 1518.38636363636, + }, + Object { + "x": 1607437140000, + "y": 930.9, + }, + Object { + "x": 1607437200000, + "y": 868.230769230769, + }, + Object { + "x": 1607437260000, + "y": 892.842105263158, + }, + Object { + "x": 1607437320000, + "y": 995.021739130435, + }, + Object { + "x": 1607437380000, + "y": 3405.75, + }, + Object { + "x": 1607437440000, + "y": 976.102564102564, + }, + Object { + "x": 1607437500000, + "y": 1167.9, + }, + Object { + "x": 1607437560000, + "y": 839.463414634146, + }, + Object { + "x": 1607437620000, + "y": 775.857142857143, + }, + ], + "value": 1135.15508885299, + }, + "throughput": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 1.66666666666667, + }, + Object { + "x": 1607436780000, + "y": 2.73333333333333, + }, + Object { + "x": 1607436840000, + "y": 2.53333333333333, + }, + Object { + "x": 1607436900000, + "y": 3.13333333333333, + }, + Object { + "x": 1607436960000, + "y": 2.26666666666667, + }, + Object { + "x": 1607437020000, + "y": 2.86666666666667, + }, + Object { + "x": 1607437080000, + "y": 2.93333333333333, + }, + Object { + "x": 1607437140000, + "y": 2.66666666666667, + }, + Object { + "x": 1607437200000, + "y": 2.6, + }, + Object { + "x": 1607437260000, + "y": 2.53333333333333, + }, + Object { + "x": 1607437320000, + "y": 3.06666666666667, + }, + Object { + "x": 1607437380000, + "y": 2.4, + }, + Object { + "x": 1607437440000, + "y": 2.6, + }, + Object { + "x": 1607437500000, + "y": 2.66666666666667, + }, + Object { + "x": 1607437560000, + "y": 2.73333333333333, + }, + Object { + "x": 1607437620000, + "y": 1.86666666666667, + }, + ], + "value": 41.2666666666667, + }, + }, + "name": "redis", + "previousPeriodMetrics": Object { + "errorRate": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 0, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 0, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], + "value": 0, + }, + "impact": 1.59711836133489, + "latency": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 1357.125, + }, + Object { + "x": 1607436780000, + "y": 900.184210526316, + }, + Object { + "x": 1607436840000, + "y": 902.975609756098, + }, + Object { + "x": 1607436900000, + "y": 769.7, + }, + Object { + "x": 1607436960000, + "y": 1246.225, + }, + Object { + "x": 1607437020000, + "y": 912.025641025641, + }, + Object { + "x": 1607437080000, + "y": 769.05, + }, + Object { + "x": 1607437140000, + "y": 901.617647058824, + }, + Object { + "x": 1607437200000, + "y": 1168.02272727273, + }, + Object { + "x": 1607437260000, + "y": 979.815789473684, + }, + Object { + "x": 1607437320000, + "y": 1151.80434782609, + }, + Object { + "x": 1607437380000, + "y": 803.117647058824, + }, + Object { + "x": 1607437440000, + "y": 774.883720930233, + }, + Object { + "x": 1607437500000, + "y": 931.325581395349, + }, + Object { + "x": 1607437560000, + "y": 845.153846153846, + }, + Object { + "x": 1607437620000, + "y": 1097.13333333333, + }, + ], + "value": 949.938333333333, + }, + "throughput": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 1.06666666666667, + }, + Object { + "x": 1607436780000, + "y": 2.53333333333333, + }, + Object { + "x": 1607436840000, + "y": 2.73333333333333, + }, + Object { + "x": 1607436900000, + "y": 3.33333333333333, + }, + Object { + "x": 1607436960000, + "y": 2.66666666666667, + }, + Object { + "x": 1607437020000, + "y": 2.6, + }, + Object { + "x": 1607437080000, + "y": 2.66666666666667, + }, + Object { + "x": 1607437140000, + "y": 2.26666666666667, + }, + Object { + "x": 1607437200000, + "y": 2.93333333333333, + }, + Object { + "x": 1607437260000, + "y": 2.53333333333333, + }, + Object { + "x": 1607437320000, + "y": 3.06666666666667, + }, + Object { + "x": 1607437380000, + "y": 2.26666666666667, + }, + Object { + "x": 1607437440000, + "y": 2.86666666666667, + }, + Object { + "x": 1607437500000, + "y": 2.86666666666667, + }, + Object { + "x": 1607437560000, + "y": 2.6, + }, + Object { + "x": 1607437620000, + "y": 1, + }, + ], + "value": 40, + }, + }, + "spanSubtype": "redis", + "spanType": "db", + "type": "external", +} +`; + +exports[`APM API tests basic no data Service overview dependencies when specific data is loaded returns opbeans-node as a dependency 1`] = ` +Object { + "agentName": "nodejs", + "currentPeriodMetrics": Object { + "errorRate": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 0.333333333333333, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], + "value": 0.25, + }, + "impact": 100, + "latency": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 6.66666666666667, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": 1, + }, + ], + "value": 5.25, + }, + "throughput": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 0.2, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": 0.0666666666666667, + }, + ], + "value": 0.266666666666667, + }, + }, + "environment": "", + "name": "opbeans-node", + "previousPeriodMetrics": Object { + "errorRate": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": 0.333333333333333, + }, + ], + "value": 0.333333333333333, + }, + "impact": 100, + "latency": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": 6.66666666666667, + }, + ], + "value": 6.66666666666667, + }, + "throughput": Object { + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": 0.2, + }, + ], + "value": 0.2, + }, + }, + "serviceName": "opbeans-node", + "type": "service", +} +`; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts index 142802840974d..f7115b97c893a 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; -import { last, omit, pick, sortBy } from 'lodash'; +import { last, pick, sortBy } from 'lodash'; import { ValuesType } from 'utility-types'; +import moment from 'moment'; import { createApmApiSupertest } from '../../../common/apm_api_supertest'; import { roundNumber } from '../../../utils'; import { ENVIRONMENT_ALL } from '../../../../../plugins/apm/common/environment_filter_values'; @@ -22,7 +23,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { const es = getService('es'); const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; + const timeRange = archives[archiveName]; + const comparisonStart = timeRange.start; + const comparisonEnd = moment(timeRange.start).add(15, 'minutes').toISOString(); + + const start = moment(timeRange.end).subtract(15, 'minutes').toISOString(); + const end = timeRange.end; registry.when( 'Service overview dependencies when data is not loaded', @@ -38,12 +44,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, + comparisonStart, + comparisonEnd, }, }, }); expect(response.status).to.be(200); - expect(response.body.serviceDependencies).to.eql([]); + expect(response.body).to.eql({ serviceDependencies: [] }); }); } ); @@ -213,6 +221,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, + comparisonStart, + comparisonEnd, }, }, }); @@ -222,7 +232,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); }); - it('returns two dependencies', () => { + it('returns correct number of dependencies', () => { expect(response.body.serviceDependencies.length).to.be(2); }); @@ -233,33 +243,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(opbeansNode !== undefined).to.be(true); - const values = { - latency: roundNumber(opbeansNode?.latency.value), - throughput: roundNumber(opbeansNode?.throughput.value), - errorRate: roundNumber(opbeansNode?.errorRate.value), - ...pick(opbeansNode, 'serviceName', 'type', 'agentName', 'environment', 'impact'), - }; - - const count = 4; - const sum = 21; - const errors = 1; - - expect(values).to.eql({ - agentName: 'nodejs', - environment: '', - serviceName: 'opbeans-node', - type: 'service', - errorRate: roundNumber(errors / count), - latency: roundNumber(sum / count), - throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), - impact: 100, - }); + expectSnapshot(opbeansNode).toMatch(); - const firstValue = roundNumber(opbeansNode?.latency.timeseries[0].y); - const lastValue = roundNumber(last(opbeansNode?.latency.timeseries)?.y); + const currentPeriodFirstValue = roundNumber( + opbeansNode?.currentPeriodMetrics.latency.timeseries[0].y + ); + const currentPeriodLastValue = roundNumber( + last(opbeansNode?.previousPeriodMetrics?.latency?.timeseries)?.y + ); - expect(firstValue).to.be(roundNumber(20 / 3)); - expect(lastValue).to.be('1.000'); + expectSnapshot(currentPeriodFirstValue).toMatchInline(`"6.667"`); + expectSnapshot(currentPeriodLastValue).toMatchInline(`"6.667"`); }); it('returns postgres as an external dependency', () => { @@ -269,27 +263,48 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(postgres !== undefined).to.be(true); - const values = { - latency: roundNumber(postgres?.latency.value), - throughput: roundNumber(postgres?.throughput.value), - errorRate: roundNumber(postgres?.errorRate.value), - ...pick(postgres, 'spanType', 'spanSubtype', 'name', 'impact', 'type'), + const fields = ['spanType', 'spanSubtype', 'name', 'type']; + const currentValues = { + latency: roundNumber(postgres?.currentPeriodMetrics.latency.value), + throughput: roundNumber(postgres?.currentPeriodMetrics.throughput.value), + errorRate: roundNumber(postgres?.currentPeriodMetrics.errorRate.value), + impact: postgres?.currentPeriodMetrics.impact, + ...pick(postgres, ...fields), }; - const count = 1; - const sum = 3; - const errors = 0; - - expect(values).to.eql({ - spanType: 'external', - spanSubtype: 'http', - name: 'postgres', - type: 'external', - errorRate: roundNumber(errors / count), - latency: roundNumber(sum / count), - throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), - impact: 0, - }); + expectSnapshot(currentValues).toMatchInline(` + Object { + "errorRate": "0.000", + "impact": 0, + "latency": "3.000", + "name": "postgres", + "spanSubtype": "http", + "spanType": "external", + "throughput": "0.06667", + "type": "external", + } + `); + + const previousValues = { + latency: roundNumber(postgres?.previousPeriodMetrics.latency?.value), + throughput: roundNumber(postgres?.previousPeriodMetrics.throughput?.value), + errorRate: roundNumber(postgres?.previousPeriodMetrics.errorRate?.value), + impact: postgres?.previousPeriodMetrics?.impact, + ...pick(postgres, ...fields), + }; + + expectSnapshot(previousValues).toMatchInline(` + Object { + "errorRate": "0.000", + "impact": 0, + "latency": "3.000", + "name": "postgres", + "spanSubtype": "http", + "spanType": "external", + "throughput": "0.06667", + "type": "external", + } + `); }); } ); @@ -313,6 +328,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, + comparisonStart, + comparisonEnd, }, }, }); @@ -324,31 +341,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns at least one item', () => { expect(response.body.serviceDependencies.length).to.be.greaterThan(0); - - expectSnapshot( - omit(response.body.serviceDependencies[0], [ - 'errorRate.timeseries', - 'throughput.timeseries', - 'latency.timeseries', - ]) - ).toMatchInline(` - Object { - "errorRate": Object { - "value": 0, - }, - "impact": 1.97910470896139, - "latency": Object { - "value": 1043.99015586546, - }, - "name": "redis", - "spanSubtype": "redis", - "spanType": "db", - "throughput": Object { - "value": 40.6333333333333, - }, - "type": "external", - } - `); + expectSnapshot(response.body.serviceDependencies[0]).toMatch(); }); it('returns the right names', () => { @@ -379,7 +372,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const latencyValues = sortBy( response.body.serviceDependencies.map((item) => ({ name: item.name, - latency: item.latency.value, + currentPeriodLatency: item.currentPeriodMetrics.latency.value, + previousPeriodLatency: item.previousPeriodMetrics?.latency?.value, })), 'name' ); @@ -387,20 +381,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(latencyValues).toMatchInline(` Array [ Object { - "latency": 2568.40816326531, + "currentPeriodLatency": 2628.905, "name": "elasticsearch", + "previousPeriodLatency": 2505.390625, }, Object { - "latency": 25593.875, + "currentPeriodLatency": 27859.2857142857, "name": "opbeans-java", + "previousPeriodLatency": 23831.8888888889, }, Object { - "latency": 28885.3293963255, + "currentPeriodLatency": 28580.1312997347, "name": "postgresql", + "previousPeriodLatency": 29184.1857142857, }, Object { - "latency": 1043.99015586546, + "currentPeriodLatency": 1135.15508885299, "name": "redis", + "previousPeriodLatency": 949.938333333333, }, ] `); @@ -410,7 +408,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const throughputValues = sortBy( response.body.serviceDependencies.map((item) => ({ name: item.name, - throughput: item.throughput.value, + currentPeriodThroughput: item.currentPeriodMetrics.throughput.value, + previousPeriodThroughput: item.previousPeriodMetrics?.throughput?.value, })), 'name' ); @@ -418,20 +417,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(throughputValues).toMatchInline(` Array [ Object { + "currentPeriodThroughput": 13.3333333333333, "name": "elasticsearch", - "throughput": 13.0666666666667, + "previousPeriodThroughput": 12.8, }, Object { + "currentPeriodThroughput": 0.466666666666667, "name": "opbeans-java", - "throughput": 0.533333333333333, + "previousPeriodThroughput": 0.6, }, Object { + "currentPeriodThroughput": 50.2666666666667, "name": "postgresql", - "throughput": 50.8, + "previousPeriodThroughput": 51.3333333333333, }, Object { + "currentPeriodThroughput": 41.2666666666667, "name": "redis", - "throughput": 40.6333333333333, + "previousPeriodThroughput": 40, }, ] `); @@ -441,9 +444,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const impactValues = sortBy( response.body.serviceDependencies.map((item) => ({ name: item.name, - impact: item.impact, - latency: item.latency.value, - throughput: item.throughput.value, + currentPeriodImpact: item.currentPeriodMetrics.impact, + previousPeriodImpact: item.previousPeriodMetrics?.impact, })), 'name' ); @@ -451,28 +453,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(impactValues).toMatchInline(` Array [ Object { - "impact": 1.36961744704522, - "latency": 2568.40816326531, + "currentPeriodImpact": 1.54893576051104, "name": "elasticsearch", - "throughput": 13.0666666666667, + "previousPeriodImpact": 1.19757368986118, }, Object { - "impact": 0, - "latency": 25593.875, + "currentPeriodImpact": 0, "name": "opbeans-java", - "throughput": 0.533333333333333, + "previousPeriodImpact": 0, }, Object { - "impact": 100, - "latency": 28885.3293963255, + "currentPeriodImpact": 100, "name": "postgresql", - "throughput": 50.8, + "previousPeriodImpact": 100, }, Object { - "impact": 1.97910470896139, - "latency": 1043.99015586546, + "currentPeriodImpact": 2.37724265214801, "name": "redis", - "throughput": 40.6333333333333, + "previousPeriodImpact": 1.59711836133489, }, ] `);