From 515c1d3cb0e3d609eff8048848d76f2bf77ce779 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 25 Mar 2021 09:24:03 -0400 Subject: [PATCH 01/14] adding comparison to dependencies table --- .../service_overview.test.tsx | 3 + .../index.tsx | 74 +++-- .../get_service_dependencies/get_metrics.ts | 202 ++++++++---- .../get_service_dependencies/index.ts | 306 +++++++++++++----- x-pack/plugins/apm/server/routes/services.ts | 11 +- .../service_overview/dependencies/index.ts | 280 +++++++++++----- 6 files changed, 621 insertions(+), 255 deletions(-) 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 f6ffec46f9f51..7f1fc9d5df0f0 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 @@ -25,6 +25,7 @@ import { waitFor } from '@testing-library/dom'; import * as callApmApiModule from '../../../services/rest/createCallApmApi'; 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'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -51,6 +52,8 @@ function Wrapper({ children }: { children?: ReactNode }) { rangeFrom: 'now-15m', rangeTo: 'now', latencyAggregationType: LatencyAggregationType.avg, + comparisonType: TimeRangeComparisonType.DayBefore, + comparisonEnabled: true, }} > {children} 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 a4647bc148b1e..7a8ea4d1cf5e2 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 @@ -32,6 +32,7 @@ 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 { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; @@ -41,9 +42,15 @@ interface Props { export function ServiceOverviewDependenciesTable({ serviceName }: Props) { const { - urlParams: { start, end, environment }, + urlParams: { start, end, environment, comparisonType, comparisonEnabled }, } = useUrlParams(); + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + }); + const columns: Array> = [ { field: 'name', @@ -97,12 +104,15 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { } ), width: px(unit * 10), - render: (_, { latency }) => { + render: (_, { currentPeriod, previousPeriod }) => { return ( ); }, @@ -115,13 +125,18 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { { defaultMessage: 'Throughput' } ), width: px(unit * 10), - render: (_, { throughput }) => { + render: (_, { currentPeriod, previousPeriod }) => { return ( ); }, @@ -136,13 +151,18 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { } ), width: px(unit * 10), - render: (_, { errorRate }) => { + render: (_, { currentPeriod, previousPeriod }) => { return ( ); }, @@ -157,8 +177,23 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { } ), width: px(unit * 5), - render: (_, { impact }) => { - return ; + render: (_, { currentPeriod, previousPeriod }) => { + return ( + + + + + {comparisonEnabled && ( + + + + )} + + ); }, sortable: true, }, @@ -166,10 +201,9 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { const { data = [], status } = useFetcher( (callApmApi) => { - if (!start || !end) { + if (!start || !end || !comparisonStart || !comparisonEnd) { return; } - return callApmApi({ endpoint: 'GET /api/apm/services/{serviceName}/dependencies', params: { @@ -181,20 +215,22 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { end, environment, numBuckets: 20, + comparisonStart, + comparisonEnd, }, }, }); }, - [start, end, serviceName, environment] + [start, end, serviceName, environment, comparisonStart, comparisonEnd] ); // need top-level sortable fields for the managed table const items = data.map((item) => ({ ...item, - errorRateValue: item.errorRate.value, - latencyValue: item.latency.value, - throughputValue: item.throughput.value, - impactValue: item.impact, + errorRateValue: item.currentPeriod.errorRate.value, + latencyValue: item.currentPeriod.latency.value, + throughputValue: item.currentPeriod.throughput.value, + impactValue: item.currentPeriod.impact, })); return ( 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..f69a6761a8504 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 @@ -13,23 +13,70 @@ import { SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, } from '../../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../common/event_outcome'; import { ProcessorEvent } from '../../../../common/processor_event'; import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; import { getBucketSize } from '../../helpers/get_bucket_size'; -import { EventOutcome } from '../../../../common/event_outcome'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; + +function getPeriodAggregation(start: number, end: number, numBuckets: number) { + return { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ + start, + end, + numBuckets, + }).intervalString, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + latency_sum: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, + }, + }, + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + [EVENT_OUTCOME]: { + terms: { + field: EVENT_OUTCOME, + }, + aggs: { + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + }, + }, + }, + }, + }; +} export const getMetrics = ({ setup, serviceName, environment, numBuckets, + comparisonStart, + comparisonEnd, }: { setup: Setup & SetupTimeRange; serviceName: string; environment?: string; numBuckets: number; + comparisonStart: number; + comparisonEnd: number; }) => { return withApmSpan('get_service_destination_metrics', async () => { const { start, end, apmEventClient } = setup; @@ -48,7 +95,6 @@ export const getMetrics = ({ { exists: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, }, - ...rangeQuery(start, end), ...environmentQuery(environment), ], }, @@ -60,40 +106,17 @@ export const getMetrics = ({ size: 100, }, aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: getBucketSize({ start, end, numBuckets }) - .intervalString, - extended_bounds: { - min: start, - max: end, - }, - }, - aggs: { - latency_sum: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, - }, - }, - count: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, - }, - }, - [EVENT_OUTCOME]: { - terms: { - field: EVENT_OUTCOME, - }, - aggs: { - count: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, - }, - }, - }, - }, - }, + current_period: { + filter: rangeQuery(start, end)[0], + aggs: getPeriodAggregation(start, end, numBuckets), + }, + previous_period: { + filter: rangeQuery(comparisonStart, comparisonEnd)[0], + aggs: getPeriodAggregation( + comparisonStart, + comparisonEnd, + numBuckets + ), }, }, }, @@ -102,44 +125,81 @@ export const getMetrics = ({ }); return ( - response.aggregations?.connections.buckets.map((bucket) => ({ - span: { - destination: { - service: { - resource: String(bucket.key), + response.aggregations?.connections.buckets.map((bucket) => { + const { + key, + current_period: currentPeriod, + previous_period: previousPeriod, + } = bucket; + + return { + span: { destination: { service: { resource: String(key) } } }, + currentPeriod: { + value: { + count: sum( + currentPeriod.timeseries.buckets.map( + (dateBucket) => dateBucket.count.value ?? 0 + ) + ), + latency_sum: sum( + currentPeriod.timeseries.buckets.map( + (dateBucket) => dateBucket.latency_sum.value ?? 0 + ) + ), + error_count: sum( + currentPeriod.timeseries.buckets.flatMap( + (dateBucket) => + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => + outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0 + ) + ), }, + timeseries: currentPeriod.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + count: dateBucket.count.value ?? 0, + latency_sum: dateBucket.latency_sum.value ?? 0, + error_count: + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0, + })), }, - }, - value: { - count: sum( - bucket.timeseries.buckets.map( - (dateBucket) => dateBucket.count.value ?? 0 - ) - ), - latency_sum: sum( - bucket.timeseries.buckets.map( - (dateBucket) => dateBucket.latency_sum.value ?? 0 - ) - ), - error_count: sum( - bucket.timeseries.buckets.flatMap( - (dateBucket) => + previousPeriod: { + value: { + count: sum( + previousPeriod.timeseries.buckets.map( + (dateBucket) => dateBucket.count.value ?? 0 + ) + ), + latency_sum: sum( + previousPeriod.timeseries.buckets.map( + (dateBucket) => dateBucket.latency_sum.value ?? 0 + ) + ), + error_count: sum( + previousPeriod.timeseries.buckets.flatMap( + (dateBucket) => + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => + outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0 + ) + ), + }, + timeseries: previousPeriod.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + count: dateBucket.count.value ?? 0, + latency_sum: dateBucket.latency_sum.value ?? 0, + error_count: dateBucket[EVENT_OUTCOME].buckets.find( (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0 - ) - ), - }, - timeseries: bucket.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - count: dateBucket.count.value ?? 0, - latency_sum: dateBucket.latency_sum.value ?? 0, - error_count: - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0, - })), - })) ?? [] + )?.count.value ?? 0, + })), + }, + }; + }) ?? [] ); }); }; 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..cf9a33c95c75e 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 @@ -7,6 +7,7 @@ import { ValuesType } from 'utility-types'; import { merge } from 'lodash'; +import { Coordinate } from '../../../../typings/timeseries'; import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; import { maybe } from '../../../../common/utils/maybe'; import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; @@ -17,22 +18,19 @@ 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 { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate'; + +interface Metrics { + latency: { value: number | null; timeseries: Coordinate[] }; + throughput: { value: number | null; timeseries: Coordinate[] }; + errorRate: { value: number | null; timeseries: Coordinate[] }; + impact: number; +} 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; + currentPeriod: Metrics; + previousPeriod: Metrics; } & ( | { type: 'service'; @@ -43,16 +41,82 @@ export type ServiceDependencyItem = { | { type: 'external'; spanType?: string; spanSubtype?: string } ); +function getDestinationMetrics({ + start, + end, + metrics, +}: { + start: number; + end: number; + metrics: { + value: { + count: number; + latency_sum: number; + error_count: number; + }; + timeseries: Array<{ + x: number; + count: number; + latency_sum: number; + error_count: number; + }>; + }; +}) { + return { + latency: { + value: + metrics.value.count > 0 + ? metrics.value.latency_sum / metrics.value.count + : null, + timeseries: metrics.timeseries.map((point) => ({ + x: point.x, + y: point.count > 0 ? point.latency_sum / point.count : null, + })), + }, + throughput: { + value: + metrics.value.count > 0 + ? calculateThroughput({ + start, + end, + value: metrics.value.count, + }) + : null, + timeseries: metrics.timeseries.map((point) => ({ + x: point.x, + y: + point.count > 0 + ? calculateThroughput({ start, end, value: point.count }) + : null, + })), + }, + errorRate: { + value: + metrics.value.count > 0 + ? (metrics.value.error_count ?? 0) / metrics.value.count + : null, + timeseries: metrics.timeseries.map((point) => ({ + x: point.x, + y: point.count > 0 ? (point.error_count ?? 0) / point.count : null, + })), + }, + }; +} + export function getServiceDependencies({ setup, serviceName, environment, numBuckets, + comparisonStart, + comparisonEnd, }: { serviceName: string; setup: Setup & SetupTimeRange; environment?: string; numBuckets: number; + comparisonStart: number; + comparisonEnd: number; }): Promise { return withApmSpan('get_service_dependencies', async () => { const { start, end } = setup; @@ -62,6 +126,8 @@ export function getServiceDependencies({ serviceName, environment, numBuckets, + comparisonStart, + comparisonEnd, }), getDestinationMap({ setup, @@ -112,74 +178,91 @@ export function getServiceDependencies({ >( (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, + currentPeriod: { + value: { + count: + prev.currentPeriod.value.count + + current.currentPeriod.value.count, + latency_sum: + prev.currentPeriod.value.latency_sum + + current.currentPeriod.value.latency_sum, + error_count: + prev.currentPeriod.value.error_count + + current.currentPeriod.value.error_count, + }, + timeseries: joinByKey( + [ + ...prev.currentPeriod.timeseries, + ...current.currentPeriod.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, + }) + ), + }, + previousPeriod: { + value: { + count: + prev.previousPeriod.value.count + + current.previousPeriod.value.count, + latency_sum: + prev.previousPeriod.value.latency_sum + + current.previousPeriod.value.latency_sum, + error_count: + prev.previousPeriod.value.error_count + + current.previousPeriod.value.error_count, + }, + timeseries: joinByKey( + [ + ...prev.previousPeriod.timeseries, + ...current.previousPeriod.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, + }) + ), }, - 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, + currentPeriod: { + value: { + count: 0, + latency_sum: 0, + error_count: 0, + }, + timeseries: [], + }, + previousPeriod: { + value: { + count: 0, + latency_sum: 0, + error_count: 0, + }, + timeseries: [], }, - timeseries: [], } ); - 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 destMetrics = getDestinationMetrics({ + start, + end, + metrics: mergedMetrics.currentPeriod, + }); + + const previousPeriodDestMetrics = getDestinationMetrics({ + start, + end, + metrics: mergedMetrics.previousPeriod, + }); if (item.service) { return { @@ -189,7 +272,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, + currentPeriod: destMetrics, + previousPeriod: previousPeriodDestMetrics, }; } @@ -198,32 +282,82 @@ export function getServiceDependencies({ type: 'external' as const, spanType: item.span.type, spanSubtype: item.span.subtype, - ...destMetrics, + currentPeriod: destMetrics, + previousPeriod: previousPeriodDestMetrics, }; } ); - const latencySums = metricsByResolvedAddress + const currentPeriodLatencySums = metricsByResolvedAddress + .map( + (metric) => + (metric.currentPeriod.latency.value ?? 0) * + (metric.currentPeriod.throughput.value ?? 0) + ) + .filter(isFiniteNumber); + + const currentPeriodMinLatencySum = Math.min(...currentPeriodLatencySums); + const currentPeriodMaxLatencySum = Math.max(...currentPeriodLatencySums); + + const previousPeriodLatencySums = metricsByResolvedAddress .map( - (metric) => (metric.latency.value ?? 0) * (metric.throughput.value ?? 0) + (metric) => + (metric.previousPeriod.latency.value ?? 0) * + (metric.previousPeriod.throughput.value ?? 0) ) .filter(isFiniteNumber); - const minLatencySum = Math.min(...latencySums); - const maxLatencySum = Math.max(...latencySums); + const previousPeriodMinLatencySum = Math.min(...previousPeriodLatencySums); + const previousPeriodMaxLatencySum = Math.max(...previousPeriodLatencySums); return metricsByResolvedAddress.map((metric) => { - const impact = - isFiniteNumber(metric.latency.value) && - isFiniteNumber(metric.throughput.value) - ? ((metric.latency.value * metric.throughput.value - minLatencySum) / - (maxLatencySum - minLatencySum)) * + const { currentPeriod, previousPeriod, ...rest } = metric; + + const currentPeriodImpact = + isFiniteNumber(currentPeriod.latency.value) && + isFiniteNumber(currentPeriod.throughput.value) + ? ((currentPeriod.latency.value * currentPeriod.throughput.value - + currentPeriodMinLatencySum) / + (currentPeriodMaxLatencySum - currentPeriodMinLatencySum)) * + 100 + : 0; + + const previousPeriodImpact = + isFiniteNumber(previousPeriod.latency.value) && + isFiniteNumber(previousPeriod.throughput.value) + ? ((previousPeriod.latency.value * previousPeriod.throughput.value - + previousPeriodMinLatencySum) / + (previousPeriodMaxLatencySum - previousPeriodMinLatencySum)) * 100 : 0; return { - ...metric, - impact, + ...rest, + currentPeriod: { ...currentPeriod, impact: currentPeriodImpact }, + previousPeriod: { + latency: { + ...previousPeriod.latency, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod.latency.timeseries, + previousPeriodTimeseries: previousPeriod.latency.timeseries, + }), + }, + throughput: { + ...previousPeriod.throughput, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod.throughput.timeseries, + previousPeriodTimeseries: previousPeriod.throughput.timeseries, + }), + }, + errorRate: { + ...previousPeriod.errorRate, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod.errorRate.timeseries, + previousPeriodTimeseries: previousPeriod.errorRate.timeseries, + }), + }, + impact: previousPeriodImpact, + }, }; }); }); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index bac970416792b..c5b2ccb295718 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -489,6 +489,8 @@ export const serviceDependenciesRoute = createRoute({ query: t.intersection([ t.type({ numBuckets: toNumberRt, + comparisonStart: isoToEpochRt, + comparisonEnd: isoToEpochRt, }), environmentRt, rangeRt, @@ -501,13 +503,20 @@ export const serviceDependenciesRoute = createRoute({ const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - const { environment, numBuckets } = context.params.query; + const { + environment, + numBuckets, + comparisonStart, + comparisonEnd, + } = context.params.query; return getServiceDependencies({ serviceName, environment, setup, numBuckets, + comparisonStart, + comparisonEnd, }); }, }); 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 fde1210551816..a70bcdfaf8a24 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 @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { last, omit, pick, sortBy } from 'lodash'; import url from 'url'; import { ValuesType } from 'utility-types'; +import moment from 'moment'; import { roundNumber } from '../../../utils'; import { ENVIRONMENT_ALL } from '../../../../../plugins/apm/common/environment_filter_values'; import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; @@ -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', @@ -37,6 +43,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, + comparisonStart, + comparisonEnd, }, }) ); @@ -211,6 +219,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, + comparisonStart, + comparisonEnd, }, }) ); @@ -232,32 +242,61 @@ 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'), + currentPeriod: { + latency: roundNumber(opbeansNode?.currentPeriod.latency.value), + throughput: roundNumber(opbeansNode?.currentPeriod.throughput.value), + errorRate: roundNumber(opbeansNode?.currentPeriod.errorRate.value), + impact: opbeansNode?.currentPeriod.impact, + }, + previousPeriod: { + latency: roundNumber(opbeansNode?.previousPeriod.latency.value), + throughput: roundNumber(opbeansNode?.previousPeriod.throughput.value), + errorRate: roundNumber(opbeansNode?.previousPeriod.errorRate.value), + impact: opbeansNode?.previousPeriod.impact, + }, + ...pick(opbeansNode, 'serviceName', 'type', 'agentName', 'environment'), }; - 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(values).toMatchInline(` + Object { + "agentName": "nodejs", + "currentPeriod": Object { + "errorRate": "0.2500", + "impact": 100, + "latency": "5.250", + "throughput": "0.2667", + }, + "environment": "", + "previousPeriod": Object { + "errorRate": "0.3333", + "impact": 100, + "latency": "6.667", + "throughput": "0.2000", + }, + "serviceName": "opbeans-node", + "type": "service", + } + `); + + const currentPeriodFirstValue = roundNumber( + opbeansNode?.currentPeriod.latency.timeseries[0].y + ); + const currentPeriodLastValue = roundNumber( + last(opbeansNode?.currentPeriod.latency.timeseries)?.y + ); - const firstValue = roundNumber(opbeansNode?.latency.timeseries[0].y); - const lastValue = roundNumber(last(opbeansNode?.latency.timeseries)?.y); + expectSnapshot(currentPeriodFirstValue).toMatchInline(`"6.667"`); + expectSnapshot(currentPeriodLastValue).toMatchInline(`"1.000"`); - expect(firstValue).to.be(roundNumber(20 / 3)); - expect(lastValue).to.be('1.000'); + const previousPeriodFirstValue = roundNumber( + opbeansNode?.previousPeriod.latency.timeseries[0].y + ); + const previousPeriodLastValue = roundNumber( + last(opbeansNode?.previousPeriod.latency.timeseries)?.y + ); + + expectSnapshot(previousPeriodFirstValue).toMatchInline(`""`); + expectSnapshot(previousPeriodLastValue).toMatchInline(`"6.667"`); }); it('returns postgres as an external dependency', () => { @@ -268,26 +307,41 @@ 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'), + currentPeriod: { + latency: roundNumber(postgres?.currentPeriod.latency.value), + throughput: roundNumber(postgres?.currentPeriod.throughput.value), + errorRate: roundNumber(postgres?.currentPeriod.errorRate.value), + impact: postgres?.currentPeriod.impact, + }, + previousPeriod: { + latency: roundNumber(postgres?.previousPeriod.latency.value), + throughput: roundNumber(postgres?.previousPeriod.throughput.value), + errorRate: roundNumber(postgres?.previousPeriod.errorRate.value), + impact: postgres?.previousPeriod.impact, + }, + ...pick(postgres, 'spanType', 'spanSubtype', 'name', 'type'), }; - 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(values).toMatchInline(` + Object { + "currentPeriod": Object { + "errorRate": "0.000", + "impact": 0, + "latency": "3.000", + "throughput": "0.06667", + }, + "name": "postgres", + "previousPeriod": Object { + "errorRate": "0.000", + "impact": 0, + "latency": "3.000", + "throughput": "0.06667", + }, + "spanSubtype": "http", + "spanType": "external", + "type": "external", + } + `); }); } ); @@ -310,6 +364,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, + comparisonStart, + comparisonEnd, }, }) ); @@ -324,25 +380,42 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot( omit(response.body[0], [ - 'errorRate.timeseries', - 'throughput.timeseries', - 'latency.timeseries', + 'currentPeriod.errorRate.timeseries', + 'currentPeriod.throughput.timeseries', + 'currentPeriod.latency.timeseries', + 'previousPeriod.errorRate.timeseries', + 'previousPeriod.throughput.timeseries', + 'previousPeriod.latency.timeseries', ]) ).toMatchInline(` Object { - "errorRate": Object { - "value": 0, - }, - "impact": 1.97910470896139, - "latency": Object { - "value": 1043.99015586546, + "currentPeriod": Object { + "errorRate": Object { + "value": 0, + }, + "impact": 2.37724265214801, + "latency": Object { + "value": 1135.15508885299, + }, + "throughput": Object { + "value": 41.2666666666667, + }, }, "name": "redis", + "previousPeriod": Object { + "errorRate": Object { + "value": 0, + }, + "impact": 1.59711836133489, + "latency": Object { + "value": 949.938333333333, + }, + "throughput": Object { + "value": 40, + }, + }, "spanSubtype": "redis", "spanType": "db", - "throughput": Object { - "value": 40.6333333333333, - }, "type": "external", } `); @@ -374,27 +447,35 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right latency values', () => { const latencyValues = sortBy( - response.body.map((item) => ({ name: item.name, latency: item.latency.value })), + response.body.map((item) => ({ + name: item.name, + currentPeriodLatency: item.currentPeriod.latency.value, + previousPeriodLatency: item.previousPeriod.latency.value, + })), 'name' ); 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, }, ] `); @@ -402,27 +483,35 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right throughput values', () => { const throughputValues = sortBy( - response.body.map((item) => ({ name: item.name, throughput: item.throughput.value })), + response.body.map((item) => ({ + name: item.name, + currentPeriodThroughput: item.currentPeriod.throughput.value, + previousPeriodThroughput: item.previousPeriod.throughput.value, + })), 'name' ); 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, }, ] `); @@ -432,9 +521,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { const impactValues = sortBy( response.body.map((item) => ({ name: item.name, - impact: item.impact, - latency: item.latency.value, - throughput: item.throughput.value, + currentPeriod: { + impact: item.currentPeriod.impact, + latency: item.currentPeriod.latency.value, + throughput: item.currentPeriod.throughput.value, + }, + previousPeriod: { + impact: item.previousPeriod.impact, + latency: item.previousPeriod.latency.value, + throughput: item.previousPeriod.throughput.value, + }, })), 'name' ); @@ -442,28 +538,56 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(impactValues).toMatchInline(` Array [ Object { - "impact": 1.36961744704522, - "latency": 2568.40816326531, + "currentPeriod": Object { + "impact": 1.54893576051104, + "latency": 2628.905, + "throughput": 13.3333333333333, + }, "name": "elasticsearch", - "throughput": 13.0666666666667, + "previousPeriod": Object { + "impact": 1.19757368986118, + "latency": 2505.390625, + "throughput": 12.8, + }, }, Object { - "impact": 0, - "latency": 25593.875, + "currentPeriod": Object { + "impact": 0, + "latency": 27859.2857142857, + "throughput": 0.466666666666667, + }, "name": "opbeans-java", - "throughput": 0.533333333333333, + "previousPeriod": Object { + "impact": 0, + "latency": 23831.8888888889, + "throughput": 0.6, + }, }, Object { - "impact": 100, - "latency": 28885.3293963255, + "currentPeriod": Object { + "impact": 100, + "latency": 28580.1312997347, + "throughput": 50.2666666666667, + }, "name": "postgresql", - "throughput": 50.8, + "previousPeriod": Object { + "impact": 100, + "latency": 29184.1857142857, + "throughput": 51.3333333333333, + }, }, Object { - "impact": 1.97910470896139, - "latency": 1043.99015586546, + "currentPeriod": Object { + "impact": 2.37724265214801, + "latency": 1135.15508885299, + "throughput": 41.2666666666667, + }, "name": "redis", - "throughput": 40.6333333333333, + "previousPeriod": Object { + "impact": 1.59711836133489, + "latency": 949.938333333333, + "throughput": 40, + }, }, ] `); From f22c24d1cfb49ebfba1cb1bba8cac661e81e7248 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 25 Mar 2021 13:44:21 -0400 Subject: [PATCH 02/14] adding should to query --- .../lib/services/get_service_dependencies/get_metrics.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 f69a6761a8504..1b7d149ca9dcd 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 @@ -97,6 +97,10 @@ export const getMetrics = ({ }, ...environmentQuery(environment), ], + should: [ + ...rangeQuery(start, end), + ...rangeQuery(comparisonStart, comparisonEnd), + ], }, }, aggs: { From e005e38dba8f0abd8e859ca92cb144d525b75ffd Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 25 Mar 2021 16:07:06 -0400 Subject: [PATCH 03/14] refactoring --- .../get_service_dependencies/get_metrics.ts | 131 ++++++--------- .../get_service_dependencies/index.ts | 155 ++++++++---------- 2 files changed, 111 insertions(+), 175 deletions(-) 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 1b7d149ca9dcd..90b002d103283 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 @@ -6,6 +6,7 @@ */ import { sum } from 'lodash'; +import { AggregationResultOf } from 'typings/elasticsearch'; import { EVENT_OUTCOME, SERVICE_NAME, @@ -30,31 +31,20 @@ function getPeriodAggregation(start: number, end: number, numBuckets: number) { end, numBuckets, }).intervalString, - extended_bounds: { - min: start, - max: end, - }, + extended_bounds: { min: start, max: end }, }, aggs: { latency_sum: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, - }, + sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM }, }, count: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, - }, + sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, }, [EVENT_OUTCOME]: { - terms: { - field: EVENT_OUTCOME, - }, + terms: { field: EVENT_OUTCOME }, aggs: { count: { - sum: { - field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, - }, + sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, }, }, }, @@ -63,7 +53,44 @@ function getPeriodAggregation(start: number, end: number, numBuckets: number) { }; } -export const getMetrics = ({ +export type PeriodAggregationResultType = AggregationResultOf< + ReturnType['timeseries'], + {} +>; + +function calculateMetrics(timeseries: PeriodAggregationResultType) { + return { + value: { + count: sum( + timeseries.buckets.map((dateBucket) => dateBucket.count.value ?? 0) + ), + latency_sum: sum( + timeseries.buckets.map( + (dateBucket) => dateBucket.latency_sum.value ?? 0 + ) + ), + error_count: sum( + timeseries.buckets.flatMap( + (dateBucket) => + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0 + ) + ), + }, + timeseries: timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + count: dateBucket.count.value ?? 0, + latency_sum: dateBucket.latency_sum.value ?? 0, + error_count: + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0, + })), + }; +} + +export const getMetrics = async ({ setup, serviceName, environment, @@ -82,9 +109,7 @@ export const getMetrics = ({ const { start, end, apmEventClient } = setup; const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.metric], - }, + apm: { events: [ProcessorEvent.metric] }, body: { track_total_hits: true, size: 0, @@ -138,70 +163,8 @@ export const getMetrics = ({ return { span: { destination: { service: { resource: String(key) } } }, - currentPeriod: { - value: { - count: sum( - currentPeriod.timeseries.buckets.map( - (dateBucket) => dateBucket.count.value ?? 0 - ) - ), - latency_sum: sum( - currentPeriod.timeseries.buckets.map( - (dateBucket) => dateBucket.latency_sum.value ?? 0 - ) - ), - error_count: sum( - currentPeriod.timeseries.buckets.flatMap( - (dateBucket) => - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => - outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0 - ) - ), - }, - timeseries: currentPeriod.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - count: dateBucket.count.value ?? 0, - latency_sum: dateBucket.latency_sum.value ?? 0, - error_count: - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0, - })), - }, - previousPeriod: { - value: { - count: sum( - previousPeriod.timeseries.buckets.map( - (dateBucket) => dateBucket.count.value ?? 0 - ) - ), - latency_sum: sum( - previousPeriod.timeseries.buckets.map( - (dateBucket) => dateBucket.latency_sum.value ?? 0 - ) - ), - error_count: sum( - previousPeriod.timeseries.buckets.flatMap( - (dateBucket) => - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => - outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0 - ) - ), - }, - timeseries: previousPeriod.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - count: dateBucket.count.value ?? 0, - latency_sum: dateBucket.latency_sum.value ?? 0, - error_count: - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0, - })), - }, + currentPeriod: calculateMetrics(currentPeriod.timeseries), + previousPeriod: calculateMetrics(previousPeriod.timeseries), }; }) ?? [] ); 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 cf9a33c95c75e..5383968d751b5 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 @@ -7,6 +7,7 @@ import { ValuesType } from 'utility-types'; import { merge } from 'lodash'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { Coordinate } from '../../../../typings/timeseries'; import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; import { maybe } from '../../../../common/utils/maybe'; @@ -41,6 +42,30 @@ export type ServiceDependencyItem = { | { type: 'external'; spanType?: string; spanSubtype?: string } ); +type GetMetricsResponse = PromiseReturnType[0]; +type PeriodsMetrics = + | GetMetricsResponse['previousPeriod'] + | GetMetricsResponse['currentPeriod']; +function reduceMetrics(acc: PeriodsMetrics, current: PeriodsMetrics) { + return { + value: { + count: acc.value.count + current.value.count, + latency_sum: acc.value.latency_sum + current.value.latency_sum, + error_count: acc.value.error_count + current.value.error_count, + }, + timeseries: joinByKey( + [...acc.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, + }) + ), + }; +} + function getDestinationMetrics({ start, end, @@ -48,19 +73,7 @@ function getDestinationMetrics({ }: { start: number; end: number; - metrics: { - value: { - count: number; - latency_sum: number; - error_count: number; - }; - timeseries: Array<{ - x: number; - count: number; - latency_sum: number; - error_count: number; - }>; - }; + metrics: ReturnType; }) { return { latency: { @@ -103,6 +116,22 @@ function getDestinationMetrics({ }; } +function calculateImpact({ + latency, + throughput, + minLatency, + maxLatency, +}: { + latency: number | null; + throughput: number | null; + minLatency: number; + maxLatency: number; +}) { + return isFiniteNumber(latency) && isFiniteNumber(throughput) + ? ((latency * throughput - minLatency) / (maxLatency - minLatency)) * 100 + : 0; +} + export function getServiceDependencies({ setup, serviceName, @@ -178,75 +207,23 @@ export function getServiceDependencies({ >( (prev, current) => { return { - currentPeriod: { - value: { - count: - prev.currentPeriod.value.count + - current.currentPeriod.value.count, - latency_sum: - prev.currentPeriod.value.latency_sum + - current.currentPeriod.value.latency_sum, - error_count: - prev.currentPeriod.value.error_count + - current.currentPeriod.value.error_count, - }, - timeseries: joinByKey( - [ - ...prev.currentPeriod.timeseries, - ...current.currentPeriod.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, - }) - ), - }, - previousPeriod: { - value: { - count: - prev.previousPeriod.value.count + - current.previousPeriod.value.count, - latency_sum: - prev.previousPeriod.value.latency_sum + - current.previousPeriod.value.latency_sum, - error_count: - prev.previousPeriod.value.error_count + - current.previousPeriod.value.error_count, - }, - timeseries: joinByKey( - [ - ...prev.previousPeriod.timeseries, - ...current.previousPeriod.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, - }) - ), - }, + currentPeriod: reduceMetrics( + prev.currentPeriod, + current.currentPeriod + ), + previousPeriod: reduceMetrics( + prev.previousPeriod, + current.previousPeriod + ), }; }, { currentPeriod: { - value: { - count: 0, - latency_sum: 0, - error_count: 0, - }, + value: { count: 0, latency_sum: 0, error_count: 0 }, timeseries: [], }, previousPeriod: { - value: { - count: 0, - latency_sum: 0, - error_count: 0, - }, + value: { count: 0, latency_sum: 0, error_count: 0 }, timeseries: [], }, } @@ -313,23 +290,19 @@ export function getServiceDependencies({ return metricsByResolvedAddress.map((metric) => { const { currentPeriod, previousPeriod, ...rest } = metric; - const currentPeriodImpact = - isFiniteNumber(currentPeriod.latency.value) && - isFiniteNumber(currentPeriod.throughput.value) - ? ((currentPeriod.latency.value * currentPeriod.throughput.value - - currentPeriodMinLatencySum) / - (currentPeriodMaxLatencySum - currentPeriodMinLatencySum)) * - 100 - : 0; + const currentPeriodImpact = calculateImpact({ + latency: currentPeriod.latency.value, + throughput: currentPeriod.throughput.value, + minLatency: currentPeriodMinLatencySum, + maxLatency: currentPeriodMaxLatencySum, + }); - const previousPeriodImpact = - isFiniteNumber(previousPeriod.latency.value) && - isFiniteNumber(previousPeriod.throughput.value) - ? ((previousPeriod.latency.value * previousPeriod.throughput.value - - previousPeriodMinLatencySum) / - (previousPeriodMaxLatencySum - previousPeriodMinLatencySum)) * - 100 - : 0; + const previousPeriodImpact = calculateImpact({ + latency: previousPeriod.latency.value, + throughput: previousPeriod.throughput.value, + minLatency: previousPeriodMinLatencySum, + maxLatency: previousPeriodMaxLatencySum, + }); return { ...rest, From ed18506cdf01adf532e21b369adcb09b79c481bb Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 1 Apr 2021 11:05:38 -0400 Subject: [PATCH 04/14] addressing PR comments --- .../get_service_dependencies/get_metrics.ts | 15 ++++---- .../get_service_dependencies/index.ts | 36 ++++++++----------- 2 files changed, 24 insertions(+), 27 deletions(-) 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 90b002d103283..4cd6818379afe 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 @@ -108,6 +108,12 @@ export const getMetrics = async ({ return withApmSpan('get_service_destination_metrics', async () => { const { start, end, apmEventClient } = setup; + const previousPeriodRangeQuery = rangeQuery(start, end)[0]; + const comparisonPeriodRangeQuery = rangeQuery( + comparisonStart, + comparisonEnd + )[0]; + const response = await apmEventClient.search({ apm: { events: [ProcessorEvent.metric] }, body: { @@ -122,10 +128,7 @@ export const getMetrics = async ({ }, ...environmentQuery(environment), ], - should: [ - ...rangeQuery(start, end), - ...rangeQuery(comparisonStart, comparisonEnd), - ], + should: [previousPeriodRangeQuery, comparisonPeriodRangeQuery], }, }, aggs: { @@ -136,11 +139,11 @@ export const getMetrics = async ({ }, aggs: { current_period: { - filter: rangeQuery(start, end)[0], + filter: previousPeriodRangeQuery, aggs: getPeriodAggregation(start, end, numBuckets), }, previous_period: { - filter: rangeQuery(comparisonStart, comparisonEnd)[0], + filter: comparisonPeriodRangeQuery, aggs: getPeriodAggregation( comparisonStart, comparisonEnd, 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 5383968d751b5..81b61aa4e7a28 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 @@ -286,6 +286,7 @@ export function getServiceDependencies({ const previousPeriodMinLatencySum = Math.min(...previousPeriodLatencySums); const previousPeriodMaxLatencySum = Math.max(...previousPeriodLatencySums); + const metricKeys = ['latency', 'throughput', 'errorRate'] as const; return metricsByResolvedAddress.map((metric) => { const { currentPeriod, previousPeriod, ...rest } = metric; @@ -303,32 +304,25 @@ export function getServiceDependencies({ minLatency: previousPeriodMinLatencySum, maxLatency: previousPeriodMaxLatencySum, }); + const offSetPreviousPeriod = metricKeys.reduce( + (acc, key) => ({ + ...acc, + [key]: { + ...previousPeriod[key], + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod[key].timeseries, + previousPeriodTimeseries: previousPeriod[key].timeseries, + }), + }, + }), + {} as Metrics + ); return { ...rest, currentPeriod: { ...currentPeriod, impact: currentPeriodImpact }, previousPeriod: { - latency: { - ...previousPeriod.latency, - timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentPeriod.latency.timeseries, - previousPeriodTimeseries: previousPeriod.latency.timeseries, - }), - }, - throughput: { - ...previousPeriod.throughput, - timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentPeriod.throughput.timeseries, - previousPeriodTimeseries: previousPeriod.throughput.timeseries, - }), - }, - errorRate: { - ...previousPeriod.errorRate, - timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentPeriod.errorRate.timeseries, - previousPeriodTimeseries: previousPeriod.errorRate.timeseries, - }), - }, + ...offSetPreviousPeriod, impact: previousPeriodImpact, }, }; From f1be0d457914ee3763660db0e084f1566da471f8 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 1 Apr 2021 17:01:29 -0400 Subject: [PATCH 05/14] addressing PR comments --- .../index.tsx | 10 +- .../get_service_dependencies/get_metrics.ts | 194 ++++--- .../get_service_dependencies/index.ts | 474 ++++++++---------- 3 files changed, 310 insertions(+), 368 deletions(-) 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 79709f1795de5..007c1fca768e9 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 @@ -111,7 +111,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { series={currentPeriod.latency.timeseries} valueLabel={asMillisecondDuration(currentPeriod.latency.value)} comparisonSeries={ - comparisonEnabled ? previousPeriod.latency.timeseries : undefined + comparisonEnabled ? previousPeriod.latency : undefined } /> ); @@ -133,9 +133,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { series={currentPeriod.throughput.timeseries} valueLabel={asTransactionRate(currentPeriod.throughput.value)} comparisonSeries={ - comparisonEnabled - ? previousPeriod.throughput.timeseries - : undefined + comparisonEnabled ? previousPeriod.throughput : undefined } /> ); @@ -159,9 +157,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { series={currentPeriod.errorRate.timeseries} valueLabel={asPercent(currentPeriod.errorRate.value, 1)} comparisonSeries={ - comparisonEnabled - ? previousPeriod.errorRate.timeseries - : undefined + comparisonEnabled ? previousPeriod.errorRate : undefined } /> ); 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 4cd6818379afe..9dc0cfebc8735 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 @@ -6,7 +6,6 @@ */ import { sum } from 'lodash'; -import { AggregationResultOf } from 'typings/elasticsearch'; import { EVENT_OUTCOME, SERVICE_NAME, @@ -14,108 +13,35 @@ import { SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, } from '../../../../common/elasticsearch_fieldnames'; -import { EventOutcome } from '../../../../common/event_outcome'; import { ProcessorEvent } from '../../../../common/processor_event'; import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getBucketSize } from '../../helpers/get_bucket_size'; +import { EventOutcome } from '../../../../common/event_outcome'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { withApmSpan } from '../../../utils/with_apm_span'; -function getPeriodAggregation(start: number, end: number, numBuckets: number) { - return { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: getBucketSize({ - start, - end, - numBuckets, - }).intervalString, - extended_bounds: { min: start, max: end }, - }, - aggs: { - latency_sum: { - sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM }, - }, - count: { - sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, - }, - [EVENT_OUTCOME]: { - terms: { field: EVENT_OUTCOME }, - aggs: { - count: { - sum: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, - }, - }, - }, - }, - }, - }; -} - -export type PeriodAggregationResultType = AggregationResultOf< - ReturnType['timeseries'], - {} ->; - -function calculateMetrics(timeseries: PeriodAggregationResultType) { - return { - value: { - count: sum( - timeseries.buckets.map((dateBucket) => dateBucket.count.value ?? 0) - ), - latency_sum: sum( - timeseries.buckets.map( - (dateBucket) => dateBucket.latency_sum.value ?? 0 - ) - ), - error_count: sum( - timeseries.buckets.flatMap( - (dateBucket) => - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0 - ) - ), - }, - timeseries: timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - count: dateBucket.count.value ?? 0, - latency_sum: dateBucket.latency_sum.value ?? 0, - error_count: - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0, - })), - }; -} - -export const getMetrics = async ({ +export const getMetrics = ({ setup, serviceName, environment, numBuckets, - comparisonStart, - comparisonEnd, + start, + end, }: { setup: Setup & SetupTimeRange; serviceName: string; environment?: string; numBuckets: number; - comparisonStart: number; - comparisonEnd: number; + start: number; + end: number; }) => { return withApmSpan('get_service_destination_metrics', async () => { - const { start, end, apmEventClient } = setup; - - const previousPeriodRangeQuery = rangeQuery(start, end)[0]; - const comparisonPeriodRangeQuery = rangeQuery( - comparisonStart, - comparisonEnd - )[0]; + const { apmEventClient } = setup; const response = await apmEventClient.search({ - apm: { events: [ProcessorEvent.metric] }, + apm: { + events: [ProcessorEvent.metric], + }, body: { track_total_hits: true, size: 0, @@ -126,9 +52,9 @@ export const getMetrics = async ({ { exists: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, }, + ...rangeQuery(start, end), ...environmentQuery(environment), ], - should: [previousPeriodRangeQuery, comparisonPeriodRangeQuery], }, }, aggs: { @@ -138,17 +64,40 @@ export const getMetrics = async ({ size: 100, }, aggs: { - current_period: { - filter: previousPeriodRangeQuery, - aggs: getPeriodAggregation(start, end, numBuckets), - }, - previous_period: { - filter: comparisonPeriodRangeQuery, - aggs: getPeriodAggregation( - comparisonStart, - comparisonEnd, - numBuckets - ), + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ start, end, numBuckets }) + .intervalString, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + latency_sum: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, + }, + }, + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + [EVENT_OUTCOME]: { + terms: { + field: EVENT_OUTCOME, + }, + aggs: { + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + }, + }, + }, }, }, }, @@ -157,19 +106,44 @@ export const getMetrics = async ({ }); return ( - response.aggregations?.connections.buckets.map((bucket) => { - const { - key, - current_period: currentPeriod, - previous_period: previousPeriod, - } = bucket; - - return { - span: { destination: { service: { resource: String(key) } } }, - currentPeriod: calculateMetrics(currentPeriod.timeseries), - previousPeriod: calculateMetrics(previousPeriod.timeseries), - }; - }) ?? [] + response.aggregations?.connections.buckets.map((bucket) => ({ + span: { + destination: { + service: { + resource: String(bucket.key), + }, + }, + }, + value: { + count: sum( + bucket.timeseries.buckets.map( + (dateBucket) => dateBucket.count.value ?? 0 + ) + ), + latency_sum: sum( + bucket.timeseries.buckets.map( + (dateBucket) => dateBucket.latency_sum.value ?? 0 + ) + ), + error_count: sum( + bucket.timeseries.buckets.flatMap( + (dateBucket) => + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0 + ) + ), + }, + timeseries: bucket.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + count: dateBucket.count.value ?? 0, + latency_sum: dateBucket.latency_sum.value ?? 0, + error_count: + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0, + })), + })) ?? [] ); }); }; 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 81b61aa4e7a28..e72aee4d1a346 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 @@ -6,9 +6,9 @@ */ import { ValuesType } from 'utility-types'; -import { merge } from 'lodash'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { keyBy, merge, uniq } from 'lodash'; import { Coordinate } from '../../../../typings/timeseries'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; import { maybe } from '../../../../common/utils/maybe'; import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; @@ -21,17 +21,24 @@ import { calculateThroughput } from '../../helpers/calculate_throughput'; import { withApmSpan } from '../../../utils/with_apm_span'; import { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate'; -interface Metrics { +interface CurrentPeriodMetrics { latency: { value: number | null; timeseries: Coordinate[] }; throughput: { value: number | null; timeseries: Coordinate[] }; errorRate: { value: number | null; timeseries: Coordinate[] }; impact: number; } +interface PreviousPeriodMetrics { + latency: Coordinate[]; + throughput: Coordinate[]; + errorRate: Coordinate[]; + impact: number; +} + export type ServiceDependencyItem = { name: string; - currentPeriod: Metrics; - previousPeriod: Metrics; + currentPeriod: CurrentPeriodMetrics; + previousPeriod: PreviousPeriodMetrics; } & ( | { type: 'service'; @@ -42,94 +49,153 @@ export type ServiceDependencyItem = { | { type: 'external'; spanType?: string; spanSubtype?: string } ); -type GetMetricsResponse = PromiseReturnType[0]; -type PeriodsMetrics = - | GetMetricsResponse['previousPeriod'] - | GetMetricsResponse['currentPeriod']; -function reduceMetrics(acc: PeriodsMetrics, current: PeriodsMetrics) { - return { - value: { - count: acc.value.count + current.value.count, - latency_sum: acc.value.latency_sum + current.value.latency_sum, - error_count: acc.value.error_count + current.value.error_count, - }, - timeseries: joinByKey( - [...acc.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, - }) - ), - }; -} +function calculateMetrics( + metrics: PromiseReturnType, + destinationMap: PromiseReturnType, + start: number, + end: number +) { + const metricsWithDestinationIds = metrics.map((metricItem) => { + const spanDestination = metricItem.span.destination.service.resource; -function getDestinationMetrics({ - start, - end, - metrics, -}: { - start: number; - end: number; - metrics: ReturnType; -}) { - return { - latency: { - value: - metrics.value.count > 0 - ? metrics.value.latency_sum / metrics.value.count - : null, - timeseries: metrics.timeseries.map((point) => ({ - x: point.x, - y: point.count > 0 ? point.latency_sum / point.count : null, - })), - }, - throughput: { - value: - metrics.value.count > 0 - ? calculateThroughput({ - start, - end, - value: metrics.value.count, + 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 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, }) - : null, - timeseries: metrics.timeseries.map((point) => ({ - x: point.x, - y: - point.count > 0 - ? calculateThroughput({ start, end, value: point.count }) + ), + }; + }, + { value: { count: 0, latency_sum: 0, error_count: 0 }, timeseries: [] } + ); + + const destMetrics = { + latency: { + value: + mergedMetrics.value.count > 0 + ? mergedMetrics.value.latency_sum / mergedMetrics.value.count : null, - })), - }, - errorRate: { - value: - metrics.value.count > 0 - ? (metrics.value.error_count ?? 0) / metrics.value.count - : null, - timeseries: metrics.timeseries.map((point) => ({ - x: point.x, - y: point.count > 0 ? (point.error_count ?? 0) / point.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, + })), + }, + }; -function calculateImpact({ - latency, - throughput, - minLatency, - maxLatency, -}: { - latency: number | null; - throughput: number | null; - minLatency: number; - maxLatency: number; -}) { - return isFiniteNumber(latency) && isFiniteNumber(throughput) - ? ((latency * throughput - minLatency) / (maxLatency - minLatency)) * 100 - : 0; + if (item.service) { + return { + name: item.service.name, + type: 'service' as const, + serviceName: item.service.name, + environment: item.service.environment, + // agent.name should always be there, type returned from joinByKey is too pessimistic + agentName: item.agent!.name, + ...destMetrics, + }; + } + + return { + name: item.span.destination.service.resource, + type: 'external' as const, + spanType: item.span.type, + spanSubtype: item.span.subtype, + ...destMetrics, + }; + }); + + 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); + + 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; + + return { + ...metric, + impact, + }; + }); } export function getServiceDependencies({ @@ -149,181 +215,87 @@ export function getServiceDependencies({ }): Promise { return withApmSpan('get_service_dependencies', async () => { const { start, end } = setup; - const [allMetrics, destinationMap] = await Promise.all([ + const commonProps = { + setup, + serviceName, + environment, + numBuckets, + }; + const [ + currentPeriodMetrics, + previousPeriodMetrics, + destinationMap, + ] = await Promise.all([ + getMetrics({ ...commonProps, start, end }), getMetrics({ - setup, - serviceName, - environment, - numBuckets, - comparisonStart, - comparisonEnd, - }), - getDestinationMap({ - setup, - serviceName, - environment, + ...commonProps, + start: comparisonStart, + end: comparisonEnd, }), + 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 currentPeriod = calculateMetrics( + currentPeriodMetrics, + destinationMap, + start, + end ); - - const metricsByResolvedAddress = metricsJoinedByDestinationId.map( - (item) => { - const mergedMetrics = item.metrics.reduce< - Omit, 'span'> - >( - (prev, current) => { - return { - currentPeriod: reduceMetrics( - prev.currentPeriod, - current.currentPeriod - ), - previousPeriod: reduceMetrics( - prev.previousPeriod, - current.previousPeriod - ), - }; - }, - { - currentPeriod: { - value: { count: 0, latency_sum: 0, error_count: 0 }, - timeseries: [], - }, - previousPeriod: { - value: { count: 0, latency_sum: 0, error_count: 0 }, - timeseries: [], - }, - } - ); - - const destMetrics = getDestinationMetrics({ - start, - end, - metrics: mergedMetrics.currentPeriod, - }); - - const previousPeriodDestMetrics = getDestinationMetrics({ - start, - end, - metrics: mergedMetrics.previousPeriod, - }); - - if (item.service) { - return { - name: item.service.name, - type: 'service' as const, - serviceName: item.service.name, - environment: item.service.environment, - // agent.name should always be there, type returned from joinByKey is too pessimistic - agentName: item.agent!.name, - currentPeriod: destMetrics, - previousPeriod: previousPeriodDestMetrics, - }; - } - - return { - name: item.span.destination.service.resource, - type: 'external' as const, - spanType: item.span.type, - spanSubtype: item.span.subtype, - currentPeriod: destMetrics, - previousPeriod: previousPeriodDestMetrics, - }; - } + const previousPeriod = calculateMetrics( + previousPeriodMetrics, + destinationMap, + comparisonStart, + comparisonEnd ); - const currentPeriodLatencySums = metricsByResolvedAddress - .map( - (metric) => - (metric.currentPeriod.latency.value ?? 0) * - (metric.currentPeriod.throughput.value ?? 0) - ) - .filter(isFiniteNumber); - - const currentPeriodMinLatencySum = Math.min(...currentPeriodLatencySums); - const currentPeriodMaxLatencySum = Math.max(...currentPeriodLatencySums); + const currentPeriodGroupedByName = keyBy(currentPeriod, 'name'); + const previousPeriodGroupedByName = keyBy(previousPeriod, 'name'); - const previousPeriodLatencySums = metricsByResolvedAddress - .map( - (metric) => - (metric.previousPeriod.latency.value ?? 0) * - (metric.previousPeriod.throughput.value ?? 0) - ) - .filter(isFiniteNumber); - - const previousPeriodMinLatencySum = Math.min(...previousPeriodLatencySums); - const previousPeriodMaxLatencySum = Math.max(...previousPeriodLatencySums); - const metricKeys = ['latency', 'throughput', 'errorRate'] as const; + const uniqDependencyNames = uniq([ + ...currentPeriod.map(({ name }) => name), + ...previousPeriod.map(({ name }) => name), + ]); - return metricsByResolvedAddress.map((metric) => { - const { currentPeriod, previousPeriod, ...rest } = metric; + return uniqDependencyNames.map((name) => { + const { + latency: currentLatency, + throughput: currentThroughput, + errorRate: currentErrorRate, + impact: currentImpact, + ...currentDetailsWithoutMetrics + } = currentPeriodGroupedByName[name] || {}; - const currentPeriodImpact = calculateImpact({ - latency: currentPeriod.latency.value, - throughput: currentPeriod.throughput.value, - minLatency: currentPeriodMinLatencySum, - maxLatency: currentPeriodMaxLatencySum, - }); - - const previousPeriodImpact = calculateImpact({ - latency: previousPeriod.latency.value, - throughput: previousPeriod.throughput.value, - minLatency: previousPeriodMinLatencySum, - maxLatency: previousPeriodMaxLatencySum, - }); - const offSetPreviousPeriod = metricKeys.reduce( - (acc, key) => ({ - ...acc, - [key]: { - ...previousPeriod[key], - timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentPeriod[key].timeseries, - previousPeriodTimeseries: previousPeriod[key].timeseries, - }), - }, - }), - {} as Metrics - ); + const { + latency: previousLatency, + throughput: previousThroughput, + errorRate: previousErrorRate, + impact: previousImpact, + ...previousDetailsWithoutMetrics + } = previousPeriodGroupedByName[name] || {}; return { - ...rest, - currentPeriod: { ...currentPeriod, impact: currentPeriodImpact }, + ...previousDetailsWithoutMetrics, + ...currentDetailsWithoutMetrics, + currentPeriod: { + latency: currentLatency, + throughput: currentThroughput, + errorRate: currentErrorRate, + impact: currentImpact, + }, previousPeriod: { - ...offSetPreviousPeriod, - impact: previousPeriodImpact, + latency: offsetPreviousPeriodCoordinates({ + previousPeriodTimeseries: previousLatency?.timeseries, + currentPeriodTimeseries: currentLatency?.timeseries, + }), + throughput: offsetPreviousPeriodCoordinates({ + previousPeriodTimeseries: previousThroughput?.timeseries, + currentPeriodTimeseries: currentThroughput?.timeseries, + }), + errorRate: offsetPreviousPeriodCoordinates({ + previousPeriodTimeseries: previousErrorRate?.timeseries, + currentPeriodTimeseries: currentErrorRate?.timeseries, + }), + impact: previousImpact, }, }; }); From 61f0c84c5e37ff8fd2d060eeec713cc8a6c349d5 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 1 Apr 2021 17:16:03 -0400 Subject: [PATCH 06/14] fixing test --- .../dependencies/__snapshots__/index.snap | 842 ++++++++++++++++++ .../service_overview/dependencies/index.ts | 113 +-- 2 files changed, 846 insertions(+), 109 deletions(-) create mode 100644 x-pack/test/apm_api_integration/tests/service_overview/dependencies/__snapshots__/index.snap 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..97864f9c72139 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/__snapshots__/index.snap @@ -0,0 +1,842 @@ +// 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 { + "currentPeriod": 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", + "previousPeriod": Object { + "errorRate": 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, + }, + ], + "impact": 1.59711836133489, + "latency": 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, + }, + ], + "throughput": 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, + }, + ], + }, + "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", + "currentPeriod": 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", + "previousPeriod": Object { + "errorRate": 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, + }, + ], + "impact": 100, + "latency": 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, + }, + ], + "throughput": 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, + }, + ], + }, + "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 31e09f9e77f28..06bbb45f8889b 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 @@ -243,42 +243,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(opbeansNode !== undefined).to.be(true); - const values = { - currentPeriod: { - latency: roundNumber(opbeansNode?.currentPeriod.latency.value), - throughput: roundNumber(opbeansNode?.currentPeriod.throughput.value), - errorRate: roundNumber(opbeansNode?.currentPeriod.errorRate.value), - impact: opbeansNode?.currentPeriod.impact, - }, - previousPeriod: { - latency: roundNumber(opbeansNode?.previousPeriod.latency.value), - throughput: roundNumber(opbeansNode?.previousPeriod.throughput.value), - errorRate: roundNumber(opbeansNode?.previousPeriod.errorRate.value), - impact: opbeansNode?.previousPeriod.impact, - }, - ...pick(opbeansNode, 'serviceName', 'type', 'agentName', 'environment'), - }; - - expectSnapshot(values).toMatchInline(` - Object { - "agentName": "nodejs", - "currentPeriod": Object { - "errorRate": "0.2500", - "impact": 100, - "latency": "5.250", - "throughput": "0.2667", - }, - "environment": "", - "previousPeriod": Object { - "errorRate": "0.3333", - "impact": 100, - "latency": "6.667", - "throughput": "0.2000", - }, - "serviceName": "opbeans-node", - "type": "service", - } - `); + expectSnapshot(opbeansNode).toMatch(); const currentPeriodFirstValue = roundNumber( opbeansNode?.currentPeriod.latency.timeseries[0].y @@ -290,12 +255,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(currentPeriodFirstValue).toMatchInline(`"6.667"`); expectSnapshot(currentPeriodLastValue).toMatchInline(`"1.000"`); - const previousPeriodFirstValue = roundNumber( - opbeansNode?.previousPeriod.latency.timeseries[0].y - ); - const previousPeriodLastValue = roundNumber( - last(opbeansNode?.previousPeriod.latency.timeseries)?.y - ); + const previousPeriodFirstValue = roundNumber(opbeansNode?.previousPeriod.latency[0].y); + const previousPeriodLastValue = roundNumber(last(opbeansNode?.previousPeriod.latency)?.y); expectSnapshot(previousPeriodFirstValue).toMatchInline(`""`); expectSnapshot(previousPeriodLastValue).toMatchInline(`"6.667"`); @@ -316,9 +277,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { impact: postgres?.currentPeriod.impact, }, previousPeriod: { - latency: roundNumber(postgres?.previousPeriod.latency.value), - throughput: roundNumber(postgres?.previousPeriod.throughput.value), - errorRate: roundNumber(postgres?.previousPeriod.errorRate.value), impact: postgres?.previousPeriod.impact, }, ...pick(postgres, 'spanType', 'spanSubtype', 'name', 'type'), @@ -334,10 +292,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, "name": "postgres", "previousPeriod": Object { - "errorRate": "0.000", "impact": 0, - "latency": "3.000", - "throughput": "0.06667", }, "spanSubtype": "http", "spanType": "external", @@ -381,47 +336,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], [ - 'currentPeriod.errorRate.timeseries', - 'currentPeriod.throughput.timeseries', - 'currentPeriod.latency.timeseries', - 'previousPeriod.errorRate.timeseries', - 'previousPeriod.throughput.timeseries', - 'previousPeriod.latency.timeseries', - ]) - ).toMatchInline(` - Object { - "currentPeriod": Object { - "errorRate": Object { - "value": 0, - }, - "impact": 2.37724265214801, - "latency": Object { - "value": 1135.15508885299, - }, - "throughput": Object { - "value": 41.2666666666667, - }, - }, - "name": "redis", - "previousPeriod": Object { - "errorRate": Object { - "value": 0, - }, - "impact": 1.59711836133489, - "latency": Object { - "value": 949.938333333333, - }, - "throughput": Object { - "value": 40, - }, - }, - "spanSubtype": "redis", - "spanType": "db", - "type": "external", - } - `); + expectSnapshot(response.body.serviceDependencies[0]).toMatch(); }); it('returns the right names', () => { @@ -453,7 +368,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { response.body.serviceDependencies.map((item) => ({ name: item.name, currentPeriodLatency: item.currentPeriod.latency.value, - previousPeriodLatency: item.previousPeriod.latency.value, })), 'name' ); @@ -463,22 +377,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { Object { "currentPeriodLatency": 2628.905, "name": "elasticsearch", - "previousPeriodLatency": 2505.390625, }, Object { "currentPeriodLatency": 27859.2857142857, "name": "opbeans-java", - "previousPeriodLatency": 23831.8888888889, }, Object { "currentPeriodLatency": 28580.1312997347, "name": "postgresql", - "previousPeriodLatency": 29184.1857142857, }, Object { "currentPeriodLatency": 1135.15508885299, "name": "redis", - "previousPeriodLatency": 949.938333333333, }, ] `); @@ -489,7 +399,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { response.body.serviceDependencies.map((item) => ({ name: item.name, currentPeriodThroughput: item.currentPeriod.throughput.value, - previousPeriodThroughput: item.previousPeriod.throughput.value, })), 'name' ); @@ -499,22 +408,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { Object { "currentPeriodThroughput": 13.3333333333333, "name": "elasticsearch", - "previousPeriodThroughput": 12.8, }, Object { "currentPeriodThroughput": 0.466666666666667, "name": "opbeans-java", - "previousPeriodThroughput": 0.6, }, Object { "currentPeriodThroughput": 50.2666666666667, "name": "postgresql", - "previousPeriodThroughput": 51.3333333333333, }, Object { "currentPeriodThroughput": 41.2666666666667, "name": "redis", - "previousPeriodThroughput": 40, }, ] `); @@ -531,8 +436,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, previousPeriod: { impact: item.previousPeriod.impact, - latency: item.previousPeriod.latency.value, - throughput: item.previousPeriod.throughput.value, }, })), 'name' @@ -549,8 +452,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "name": "elasticsearch", "previousPeriod": Object { "impact": 1.19757368986118, - "latency": 2505.390625, - "throughput": 12.8, }, }, Object { @@ -562,8 +463,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "name": "opbeans-java", "previousPeriod": Object { "impact": 0, - "latency": 23831.8888888889, - "throughput": 0.6, }, }, Object { @@ -575,8 +474,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "name": "postgresql", "previousPeriod": Object { "impact": 100, - "latency": 29184.1857142857, - "throughput": 51.3333333333333, }, }, Object { @@ -588,8 +485,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "name": "redis", "previousPeriod": Object { "impact": 1.59711836133489, - "latency": 949.938333333333, - "throughput": 40, }, }, ] From 64043d62174601ac736ddc2d48c8f5e550dd79da Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Fri, 2 Apr 2021 09:36:37 -0400 Subject: [PATCH 07/14] fixing ts issue --- .../tests/service_overview/dependencies/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 06bbb45f8889b..394bdcedaa121 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,7 +6,7 @@ */ 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'; From dd5c277abee44112e73f6e29f943fb23542b7428 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Wed, 14 Apr 2021 13:41:56 -0400 Subject: [PATCH 08/14] refactoring comparison --- .../service_overview.test.tsx | 5 +- .../get_columns.tsx | 186 ++++++ .../index.tsx | 189 +----- .../find_common_connections.test.ts | 142 +++++ .../find_common_connections.ts | 54 ++ .../get_connections.ts | 161 ++++++ .../get_destination_map.test.ts | 124 ++++ .../get_destination_map.ts | 247 ++------ .../get_service_dependencies/get_metrics.ts | 4 +- .../get_service_dependencies/index.ts | 258 +++++---- x-pack/plugins/apm/server/routes/services.ts | 17 +- .../dependencies/__snapshots__/index.snap | 536 +++--------------- .../service_overview/dependencies/index.ts | 127 ++--- 13 files changed, 1017 insertions(+), 1033 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/get_columns.tsx create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts 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 4dc7a730f0fe0..489f2a9b37a64 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 @@ -96,7 +96,10 @@ describe('ServiceOverview', () => { totalTransactionGroups: 0, isAggregationAccurate: true, }, - 'GET /api/apm/services/{serviceName}/dependencies': [], + 'GET /api/apm/services/{serviceName}/dependencies': { + currentPeriod: [], + previousPeriod: [], + }, 'GET /api/apm/services/{serviceName}/service_overview_instances/primary_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..373f4c3bd8a24 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/get_columns.tsx @@ -0,0 +1,186 @@ +/* + * 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, + previousPeriod, + comparisonEnabled, +}: { + environment?: string; + previousPeriod: ServiceDependencies['previousPeriod']; + 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 = + previousPeriod?.[item.name]?.latency.timeseries; + return ( + + ); + }, + sortable: true, + }, + { + field: 'throughputValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnThroughput', + { defaultMessage: 'Throughput' } + ), + width: px(unit * 10), + render: (_, item) => { + const previousPeriodThroughputTimeseries = + previousPeriod?.[item.name]?.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 = + previousPeriod?.[item.name]?.errorRate.timeseries; + return ( + + ); + }, + sortable: true, + }, + { + field: 'impactValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnImpact', + { + defaultMessage: 'Impact', + } + ), + width: px(unit * 5), + render: (_, item) => { + const previousPeriodImpact = previousPeriod?.[item.name]?.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 007c1fca768e9..920fa4b1503ba 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,32 +13,26 @@ 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 { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; 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 = { + currentPeriod: [], + previousPeriod: {}, +} as ServiceDependencies; + export function ServiceOverviewDependenciesTable({ serviceName }: Props) { const { urlParams: { start, end, environment, comparisonType, comparisonEnabled }, @@ -51,151 +44,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { comparisonType, }); - 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: (_, { currentPeriod, previousPeriod }) => { - return ( - - ); - }, - sortable: true, - }, - { - field: 'throughputValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnThroughput', - { defaultMessage: 'Throughput' } - ), - width: px(unit * 10), - render: (_, { currentPeriod, previousPeriod }) => { - return ( - - ); - }, - sortable: true, - }, - { - field: 'errorRateValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnErrorRate', - { - defaultMessage: 'Error rate', - } - ), - width: px(unit * 10), - render: (_, { currentPeriod, previousPeriod }) => { - return ( - - ); - }, - sortable: true, - }, - { - field: 'impactValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnImpact', - { - defaultMessage: 'Impact', - } - ), - width: px(unit * 5), - render: (_, { currentPeriod, previousPeriod }) => { - return ( - - - - - {comparisonEnabled && ( - - - - )} - - ); - }, - sortable: true, - }, - ]; - - const { data, status } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end || !comparisonStart || !comparisonEnd) { return; @@ -220,17 +69,21 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { [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.currentPeriod.map((item) => ({ ...item, - errorRateValue: item.currentPeriod.errorRate.value, - latencyValue: item.currentPeriod.latency.value, - throughputValue: item.currentPeriod.throughput.value, - impactValue: item.currentPeriod.impact, + errorRateValue: item.errorRate.value, + latencyValue: item.latency.value, + throughputValue: item.throughput.value, + impactValue: item.impact, })); + const columns = getColumns({ + environment, + comparisonEnabled, + previousPeriod: data.previousPeriod, + }); + return ( diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts new file mode 100644 index 0000000000000..4f6bb8ab80fd2 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; +import { findCommonConnections } from './find_common_connections'; +import { Connections } from './get_connections'; + +describe('findCommonConnections', () => { + it("doesn't return connections when current and previous are empty", () => { + const currentPeriodConnections = [] as Connections; + const previousPeriodConnections = [] as Connections; + + expect( + findCommonConnections({ + currentPeriodConnections, + previousPeriodConnections, + }) + ).toEqual([]); + }); + it("doesn't return connections when current is empty", () => { + const currentPeriodConnections = [] as Connections; + const previousPeriodConnections = [ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-ruby' }, + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-java' }, + }, + ] as Connections; + + expect( + findCommonConnections({ + currentPeriodConnections, + previousPeriodConnections, + }) + ).toEqual([]); + }); + it("doesn't return connections when there are no match", () => { + const currentPeriodConnections = [ + { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-python' }, + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-dotnet' }, + }, + ] as Connections; + const previousPeriodConnections = [ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-ruby' }, + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-java' }, + }, + ] as Connections; + + expect( + findCommonConnections({ + currentPeriodConnections, + previousPeriodConnections, + }) + ).toEqual([]); + }); + it('returns connections without service name', () => { + const currentPeriodConnections = [ + { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-python' }, + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-dotnet' }, + }, + ] as Connections; + const previousPeriodConnections = [ + { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-ruby' }, + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-java' }, + }, + ] as Connections; + + expect( + findCommonConnections({ + currentPeriodConnections, + previousPeriodConnections, + }) + ).toEqual([{ [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }]); + }); + it('returns common connections', () => { + const currentPeriodConnections = [ + { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-python' }, + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-dotnet' }, + }, + ] as Connections; + const previousPeriodConnections = [ + { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-ruby' }, + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-dotnet' }, + }, + ] as Connections; + + expect( + findCommonConnections({ + currentPeriodConnections, + previousPeriodConnections, + }) + ).toEqual([ + { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + service: { name: 'opbeans-dotnet' }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts new file mode 100644 index 0000000000000..38f8f9577cc0b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts @@ -0,0 +1,54 @@ +/* + * 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 { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; +import { Connections } from './get_connections'; + +function removeUndefined( + item: Connections[0] | undefined +): item is Connections[0] { + return !!item; +} + +export function findCommonConnections({ + currentPeriodConnections, + previousPeriodConnections = [], +}: { + currentPeriodConnections: Connections; + previousPeriodConnections?: Connections; +}) { + const currentPeriodConnectionsMap: Record< + string, + Connections + > = currentPeriodConnections.reduce((acc, curr) => { + if (curr.service?.name) { + const serviceName = curr.service.name; + return { ...acc, [serviceName]: curr }; + } + const destinationSource = curr[SPAN_DESTINATION_SERVICE_RESOURCE]; + if (destinationSource) { + return { ...acc, [destinationSource]: curr }; + } + return acc; + }, {}); + + return previousPeriodConnections + .map((item) => { + const serviceName = item.service?.name; + const destinationServiceResource = + item['span.destination.service.resource']; + + if ( + (serviceName && currentPeriodConnectionsMap[serviceName]) || + (destinationServiceResource && + currentPeriodConnectionsMap[destinationServiceResource]) + ) { + return item; + } + }) + .filter(removeUndefined); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts new file mode 100644 index 0000000000000..a735e392560a4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts @@ -0,0 +1,161 @@ +/* + * 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 { + AGENT_NAME, + EVENT_OUTCOME, + PARENT_ID, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_ID, + SPAN_SUBTYPE, + SPAN_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { joinByKey } from '../../../../common/utils/join_by_key'; +import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { Setup } from '../../helpers/setup_request'; + +export type Connections = PromiseReturnType; + +export const getConnections = ({ + setup, + serviceName, + environment, + start, + end, +}: { + setup: Setup; + serviceName: string; + environment?: string; + start: number; + end: number; +}) => { + return withApmSpan('get_service_destination_map', async () => { + const { apmEventClient } = setup; + + const response = await withApmSpan('get_exit_span_samples', async () => + apmEventClient.search({ + apm: { + events: [ProcessorEvent.span], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ], + }, + }, + aggs: { + connections: { + composite: { + size: 1000, + sources: asMutableArray([ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: { + terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, + }, + }, + // make sure we get samples for both successful + // and failed calls + { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, + ] as const), + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], + sort: [ + { + '@timestamp': 'desc' as const, + }, + ], + }, + }, + }, + }, + }, + }, + }) + ); + + const outgoingConnections = + response.aggregations?.connections.buckets.map((bucket) => { + const sample = bucket.sample.hits.hits[0]._source; + + return { + [SPAN_DESTINATION_SERVICE_RESOURCE]: String( + bucket.key[SPAN_DESTINATION_SERVICE_RESOURCE] + ), + [SPAN_ID]: sample.span.id, + [SPAN_TYPE]: sample.span.type, + [SPAN_SUBTYPE]: sample.span.subtype, + }; + }) ?? []; + + const transactionResponse = await withApmSpan( + 'get_transactions_for_exit_spans', + () => + apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + { + terms: { + [PARENT_ID]: outgoingConnections.map( + (connection) => connection[SPAN_ID] + ), + }, + }, + ...rangeQuery(start, end), + ], + }, + }, + size: outgoingConnections.length, + docvalue_fields: asMutableArray([ + SERVICE_NAME, + SERVICE_ENVIRONMENT, + AGENT_NAME, + PARENT_ID, + ] as const), + _source: false, + }, + }) + ); + + const incomingConnections = transactionResponse.hits.hits.map((hit) => ({ + [SPAN_ID]: String(hit.fields[PARENT_ID]![0]), + service: { + name: String(hit.fields[SERVICE_NAME]![0]), + environment: String(hit.fields[SERVICE_ENVIRONMENT]?.[0] ?? ''), + agentName: hit.fields[AGENT_NAME]![0] as AgentName, + }, + })); + + // merge outgoing spans with transactions by span.id/parent.id + const joinedBySpanId = joinByKey( + [...outgoingConnections, ...incomingConnections], + SPAN_ID + ); + return joinedBySpanId; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts new file mode 100644 index 0000000000000..56db44d0a751f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_ID, + SPAN_SUBTYPE, + SPAN_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { Connections } from './get_connections'; +import { getDestinationMaps } from './get_destination_map'; + +describe('getDestintionMaps', () => { + it('returns empty when connections are empty', () => { + expect(getDestinationMaps([])).toEqual({}); + }); + + it('returns the destination for on connection without service', () => { + const connections = [ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql', + [SPAN_ID]: '7098b4957bd11904', + [SPAN_TYPE]: 'db', + [SPAN_SUBTYPE]: 'postgresql', + }, + ]; + expect(getDestinationMaps(connections)).toEqual({ + postgresql: { + id: { 'span.destination.service.resource': 'postgresql' }, + span: { + type: 'db', + subtype: 'postgresql', + destination: { service: { resource: 'postgresql' } }, + }, + }, + }); + }); + + it('returns the destination for on connection with service', () => { + const connections = [ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + [SPAN_ID]: 'b3c74623ee3951aa', + [SPAN_TYPE]: 'ext', + [SPAN_SUBTYPE]: 'http_rb', + service: { + name: 'opbeans-python', + environment: 'production', + agentName: 'python', + }, + }, + ] as Connections; + expect(getDestinationMaps(connections)).toEqual({ + 'opbeans:3000': { + id: { + service: { + name: 'opbeans-python', + environment: 'production', + agentName: 'python', + }, + }, + span: { + type: 'ext', + subtype: 'http_rb', + destination: { service: { resource: 'opbeans:3000' } }, + }, + service: { name: 'opbeans-python', environment: 'production' }, + agent: { name: 'python' }, + }, + }); + }); + + it('return the destination for multiples connections', () => { + const connections = [ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql', + [SPAN_ID]: '7098b4957bd11904', + [SPAN_TYPE]: 'db', + [SPAN_SUBTYPE]: 'postgresql', + }, + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', + [SPAN_ID]: 'e059b19db88ad19a', + [SPAN_TYPE]: 'ext', + [SPAN_SUBTYPE]: 'http_rb', + service: { + name: 'opbeans-dotnet', + environment: 'production', + agentName: 'dotnet', + }, + }, + ] as Connections; + + expect(getDestinationMaps(connections)).toEqual({ + postgresql: { + id: { 'span.destination.service.resource': 'postgresql' }, + span: { + type: 'db', + subtype: 'postgresql', + destination: { service: { resource: 'postgresql' } }, + }, + }, + 'opbeans:3000': { + id: { + service: { + name: 'opbeans-dotnet', + environment: 'production', + agentName: 'dotnet', + }, + }, + span: { + type: 'ext', + subtype: 'http_rb', + destination: { service: { resource: 'opbeans:3000' } }, + }, + service: { name: 'opbeans-dotnet', environment: 'production' }, + agent: { name: 'dotnet' }, + }, + }); + }); +}); 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..56ad513d5e2c8 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 @@ -6,210 +6,71 @@ */ 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 { - AGENT_NAME, - EVENT_OUTCOME, - PARENT_ID, - SERVICE_ENVIRONMENT, - SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, - SPAN_ID, SPAN_SUBTYPE, SPAN_TYPE, } from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; import { joinByKey } from '../../../../common/utils/join_by_key'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; - -export const getDestinationMap = ({ - setup, - serviceName, - environment, -}: { - setup: Setup & SetupTimeRange; - serviceName: string; - environment?: string; -}) => { - return withApmSpan('get_service_destination_map', async () => { - const { start, end, apmEventClient } = setup; +import { Connections } from './get_connections'; + +export function getDestinationMaps(connections: Connections) { + // we could have multiple connections per address because of multiple event outcomes + const dedupedConnectionsByAddress = joinByKey( + connections, + SPAN_DESTINATION_SERVICE_RESOURCE + ); + + // identify a connection by either service.name, service.environment, agent.name + // OR span.destination.service.resource + const connectionsWithId = dedupedConnectionsByAddress.map((connection) => { + const id = + 'service' in connection + ? { service: connection.service } + : pickKeys(connection, SPAN_DESTINATION_SERVICE_RESOURCE); + + return { + ...connection, + id, + }; + }); - const response = await withApmSpan('get_exit_span_samples', async () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.span], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ], - }, - }, - aggs: { - connections: { - composite: { - size: 1000, - sources: asMutableArray([ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: { - terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, - }, - }, - // make sure we get samples for both successful - // and failed calls - { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, - ] as const), - }, - aggs: { - sample: { - top_hits: { - size: 1, - _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], - sort: [ - { - '@timestamp': 'desc' as const, - }, - ], - }, - }, - }, - }, + const dedupedConnectionsById = joinByKey(connectionsWithId, 'id'); + + const connectionsByAddress = keyBy( + connectionsWithId, + SPAN_DESTINATION_SERVICE_RESOURCE + ); + + // per span.destination.service.resource, return merged/deduped item + return mapValues(connectionsByAddress, ({ id }) => { + const connection = dedupedConnectionsById.find((dedupedConnection) => + isEqual(id, dedupedConnection.id) + )!; + + return { + id, + span: { + type: connection[SPAN_TYPE], + subtype: connection[SPAN_SUBTYPE], + destination: { + service: { + resource: connection[SPAN_DESTINATION_SERVICE_RESOURCE], }, }, - }) - ); - - const outgoingConnections = - response.aggregations?.connections.buckets.map((bucket) => { - const sample = bucket.sample.hits.hits[0]._source; - - return { - [SPAN_DESTINATION_SERVICE_RESOURCE]: String( - bucket.key[SPAN_DESTINATION_SERVICE_RESOURCE] - ), - [SPAN_ID]: sample.span.id, - [SPAN_TYPE]: sample.span.type, - [SPAN_SUBTYPE]: sample.span.subtype, - }; - }) ?? []; - - const transactionResponse = await withApmSpan( - 'get_transactions_for_exit_spans', - () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - query: { - bool: { - filter: [ - { - terms: { - [PARENT_ID]: outgoingConnections.map( - (connection) => connection[SPAN_ID] - ), - }, - }, - ...rangeQuery(start, end), - ], - }, - }, - size: outgoingConnections.length, - docvalue_fields: asMutableArray([ - SERVICE_NAME, - SERVICE_ENVIRONMENT, - AGENT_NAME, - PARENT_ID, - ] as const), - _source: false, - }, - }) - ); - - const incomingConnections = transactionResponse.hits.hits.map((hit) => ({ - [SPAN_ID]: String(hit.fields[PARENT_ID]![0]), - service: { - name: String(hit.fields[SERVICE_NAME]![0]), - environment: String(hit.fields[SERVICE_ENVIRONMENT]?.[0] ?? ''), - agentName: hit.fields[AGENT_NAME]![0] as AgentName, }, - })); - - // merge outgoing spans with transactions by span.id/parent.id - const joinedBySpanId = joinByKey( - [...outgoingConnections, ...incomingConnections], - SPAN_ID - ); - - // we could have multiple connections per address because - // of multiple event outcomes - const dedupedConnectionsByAddress = joinByKey( - joinedBySpanId, - SPAN_DESTINATION_SERVICE_RESOURCE - ); - - // identify a connection by either service.name, service.environment, agent.name - // OR span.destination.service.resource - - const connectionsWithId = dedupedConnectionsByAddress.map((connection) => { - const id = - 'service' in connection - ? { service: connection.service } - : pickKeys(connection, SPAN_DESTINATION_SERVICE_RESOURCE); - - return { - ...connection, - id, - }; - }); - - const dedupedConnectionsById = joinByKey(connectionsWithId, 'id'); - - const connectionsByAddress = keyBy( - connectionsWithId, - SPAN_DESTINATION_SERVICE_RESOURCE - ); - - // per span.destination.service.resource, return merged/deduped item - return mapValues(connectionsByAddress, ({ id }) => { - const connection = dedupedConnectionsById.find((dedupedConnection) => - isEqual(id, dedupedConnection.id) - )!; - - return { - id, - span: { - type: connection[SPAN_TYPE], - subtype: connection[SPAN_SUBTYPE], - destination: { + ...('service' in connection && connection.service + ? { service: { - resource: connection[SPAN_DESTINATION_SERVICE_RESOURCE], + name: connection.service.name, + environment: connection.service.environment, }, - }, - }, - ...('service' in connection && connection.service - ? { - service: { - name: connection.service.name, - environment: connection.service.environment, - }, - agent: { - name: connection.service.agentName, - }, - } - : {}), - }; - }); + agent: { + name: connection.service.agentName, + }, + } + : {}), + }; }); -}; +} 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 9dc0cfebc8735..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 = ({ @@ -28,7 +28,7 @@ export const getMetrics = ({ start, end, }: { - setup: Setup & SetupTimeRange; + setup: Setup; serviceName: string; environment?: string; numBuckets: number; 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 e72aee4d1a346..6d6447d13490a 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,56 +5,33 @@ * 2.0. */ +import { keyBy, merge } from 'lodash'; import { ValuesType } from 'utility-types'; -import { keyBy, merge, uniq } from 'lodash'; -import { Coordinate } from '../../../../typings/timeseries'; import { PromiseReturnType } from '../../../../../observability/typings/common'; 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 { maybe } from '../../../../common/utils/maybe'; +import { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { calculateThroughput } from '../../helpers/calculate_throughput'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { findCommonConnections } from './find_common_connections'; +import { getConnections } from './get_connections'; +import { getDestinationMaps } from './get_destination_map'; 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 { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate'; - -interface CurrentPeriodMetrics { - latency: { value: number | null; timeseries: Coordinate[] }; - throughput: { value: number | null; timeseries: Coordinate[] }; - errorRate: { value: number | null; timeseries: Coordinate[] }; - impact: number; -} - -interface PreviousPeriodMetrics { - latency: Coordinate[]; - throughput: Coordinate[]; - errorRate: Coordinate[]; - impact: number; -} - -export type ServiceDependencyItem = { - name: string; - currentPeriod: CurrentPeriodMetrics; - previousPeriod: PreviousPeriodMetrics; -} & ( - | { - type: 'service'; - serviceName: string; - agentName: AgentName; - environment?: string; - } - | { type: 'external'; spanType?: string; spanSubtype?: string } -); -function calculateMetrics( - metrics: PromiseReturnType, - destinationMap: PromiseReturnType, - start: number, - end: number -) { +function calculateMetrics({ + metrics, + destinationMap, + start, + end, +}: { + metrics: PromiseReturnType; + destinationMap: ReturnType; + start: number; + end: number; +}) { const metricsWithDestinationIds = metrics.map((metricItem) => { const spanDestination = metricItem.span.destination.service.resource; @@ -109,7 +86,10 @@ function calculateMetrics( ), }; }, - { value: { count: 0, latency_sum: 0, error_count: 0 }, timeseries: [] } + { + value: { count: 0, latency_sum: 0, error_count: 0 }, + timeseries: [], + } ); const destMetrics = { @@ -198,7 +178,41 @@ function calculateMetrics( }); } -export function getServiceDependencies({ +async function getServiceMetricsAndConnections({ + setup, + serviceName, + environment, + numBuckets, + start, + end, +}: { + serviceName: string; + setup: Setup; + environment?: string; + numBuckets: number; + start: number; + end: number; +}) { + return await Promise.all([ + getMetrics({ + setup, + serviceName, + environment, + numBuckets, + start, + end, + }), + getConnections({ + setup, + serviceName, + environment, + start, + end, + }), + ]); +} + +export async function getServiceDependenciesPerPeriod({ setup, serviceName, environment, @@ -210,94 +224,96 @@ export function getServiceDependencies({ setup: Setup & SetupTimeRange; environment?: string; numBuckets: number; - comparisonStart: number; - comparisonEnd: number; -}): Promise { + comparisonStart?: number; + comparisonEnd?: number; +}) { return withApmSpan('get_service_dependencies', async () => { const { start, end } = setup; - const commonProps = { - setup, - serviceName, - environment, - numBuckets, - }; + const [ - currentPeriodMetrics, - previousPeriodMetrics, - destinationMap, + [currentPeriodMetrics, currentPeriodConnections], + [previousPeriodMetrics, previousPeriodConnections] = [], ] = await Promise.all([ - getMetrics({ ...commonProps, start, end }), - getMetrics({ - ...commonProps, - start: comparisonStart, - end: comparisonEnd, + getServiceMetricsAndConnections({ + setup, + serviceName, + environment, + numBuckets, + start, + end, }), - getDestinationMap({ setup, serviceName, environment }), + ...(comparisonStart && comparisonEnd + ? [ + getServiceMetricsAndConnections({ + setup, + serviceName, + environment, + numBuckets, + start: comparisonStart, + end: comparisonEnd, + }), + ] + : []), ]); - const currentPeriod = calculateMetrics( - currentPeriodMetrics, - destinationMap, - start, - end - ); - const previousPeriod = calculateMetrics( - previousPeriodMetrics, - destinationMap, - comparisonStart, - comparisonEnd - ); + const commonPreviousConnections = findCommonConnections({ + currentPeriodConnections, + previousPeriodConnections, + }); - const currentPeriodGroupedByName = keyBy(currentPeriod, 'name'); - const previousPeriodGroupedByName = keyBy(previousPeriod, 'name'); + const currentDestinationMap = getDestinationMaps(currentPeriodConnections); + const previousDestinationMap = getDestinationMaps( + commonPreviousConnections + ); - const uniqDependencyNames = uniq([ - ...currentPeriod.map(({ name }) => name), - ...previousPeriod.map(({ name }) => name), - ]); + const currentPeriod = calculateMetrics({ + metrics: currentPeriodMetrics, + destinationMap: currentDestinationMap, + start, + end, + }); - return uniqDependencyNames.map((name) => { - const { - latency: currentLatency, - throughput: currentThroughput, - errorRate: currentErrorRate, - impact: currentImpact, - ...currentDetailsWithoutMetrics - } = currentPeriodGroupedByName[name] || {}; + const currentPeriodMap = keyBy(currentPeriod, 'name'); - const { - latency: previousLatency, - throughput: previousThroughput, - errorRate: previousErrorRate, - impact: previousImpact, - ...previousDetailsWithoutMetrics - } = previousPeriodGroupedByName[name] || {}; + const previousPeriod = + comparisonStart && comparisonEnd && previousPeriodMetrics + ? calculateMetrics({ + metrics: previousPeriodMetrics, + destinationMap: previousDestinationMap, + start: comparisonStart, + end: comparisonEnd, + }).map((previousItem) => { + const currentItem = currentPeriodMap[previousItem.name]; + return { + ...previousItem, + latency: { + ...previousItem.latency, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentItem?.latency.timeseries, + previousPeriodTimeseries: previousItem.latency.timeseries, + }), + }, + throughput: { + ...previousItem.throughput, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentItem?.throughput.timeseries, + previousPeriodTimeseries: previousItem.throughput.timeseries, + }), + }, + errorRate: { + ...previousItem.errorRate, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentItem?.errorRate.timeseries, + previousPeriodTimeseries: previousItem.errorRate.timeseries, + }), + }, + }; + }) + : []; - return { - ...previousDetailsWithoutMetrics, - ...currentDetailsWithoutMetrics, - currentPeriod: { - latency: currentLatency, - throughput: currentThroughput, - errorRate: currentErrorRate, - impact: currentImpact, - }, - previousPeriod: { - latency: offsetPreviousPeriodCoordinates({ - previousPeriodTimeseries: previousLatency?.timeseries, - currentPeriodTimeseries: currentLatency?.timeseries, - }), - throughput: offsetPreviousPeriodCoordinates({ - previousPeriodTimeseries: previousThroughput?.timeseries, - currentPeriodTimeseries: currentThroughput?.timeseries, - }), - errorRate: offsetPreviousPeriodCoordinates({ - previousPeriodTimeseries: previousErrorRate?.timeseries, - currentPeriodTimeseries: currentErrorRate?.timeseries, - }), - impact: previousImpact, - }, - }; - }); + return { + currentPeriod, + previousPeriod: keyBy(previousPeriod, 'name'), + }; }); } diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 20a7b457e0c24..15513ce4a6333 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -18,7 +18,7 @@ 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 { getServiceDependencies } from '../lib/services/get_service_dependencies'; +import { getServiceDependenciesPerPeriod } from '../lib/services/get_service_dependencies'; import { getServiceErrorGroupPeriods } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics'; import { getServiceErrorGroupPrimaryStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_primary_statistics'; import { getServiceInstancesComparisonStatisticsPeriods } from '../lib/services/get_service_instances/comparison_statistics'; @@ -562,9 +562,8 @@ const serviceDependenciesRoute = createApmServerRoute({ query: t.intersection([ t.type({ numBuckets: toNumberRt, - comparisonStart: isoToEpochRt, - comparisonEnd: isoToEpochRt, }), + comparisonRangeRt, environmentRt, rangeRt, ]), @@ -572,18 +571,18 @@ const serviceDependenciesRoute = createApmServerRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); + handler: async (resources) => { + const setup = await setupRequest(resources); - const { serviceName } = context.params.path; + const { serviceName } = resources.params.path; const { environment, numBuckets, comparisonStart, comparisonEnd, - } = context.params.query; + } = resources.params.query; - const serviceDependencies = await getServiceDependencies({ + return getServiceDependenciesPerPeriod({ serviceName, environment, setup, @@ -591,8 +590,6 @@ const serviceDependenciesRoute = createApmServerRoute({ 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 index 97864f9c72139..6219c8d3857db 100644 --- 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 @@ -2,219 +2,8 @@ exports[`APM API tests basic apm_8.0.0 Service overview dependencies when data is loaded returns at least one item 1`] = ` Object { - "currentPeriod": 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", - "previousPeriod": Object { - "errorRate": Array [ + "errorRate": Object { + "timeseries": Array [ Object { "x": 1607436720000, "y": 0, @@ -280,109 +69,118 @@ Object { "y": 0, }, ], - "impact": 1.59711836133489, - "latency": Array [ + "value": 0, + }, + "impact": 2.37724265214801, + "latency": Object { + "timeseries": Array [ Object { "x": 1607436720000, - "y": 1357.125, + "y": 1344.68, }, Object { "x": 1607436780000, - "y": 900.184210526316, + "y": 731.439024390244, }, Object { "x": 1607436840000, - "y": 902.975609756098, + "y": 945.684210526316, }, Object { "x": 1607436900000, - "y": 769.7, + "y": 1187.1914893617, }, Object { "x": 1607436960000, - "y": 1246.225, + "y": 884.705882352941, }, Object { "x": 1607437020000, - "y": 912.025641025641, + "y": 839.604651162791, }, Object { "x": 1607437080000, - "y": 769.05, + "y": 1518.38636363636, }, Object { "x": 1607437140000, - "y": 901.617647058824, + "y": 930.9, }, Object { "x": 1607437200000, - "y": 1168.02272727273, + "y": 868.230769230769, }, Object { "x": 1607437260000, - "y": 979.815789473684, + "y": 892.842105263158, }, Object { "x": 1607437320000, - "y": 1151.80434782609, + "y": 995.021739130435, }, Object { "x": 1607437380000, - "y": 803.117647058824, + "y": 3405.75, }, Object { "x": 1607437440000, - "y": 774.883720930233, + "y": 976.102564102564, }, Object { "x": 1607437500000, - "y": 931.325581395349, + "y": 1167.9, }, Object { "x": 1607437560000, - "y": 845.153846153846, + "y": 839.463414634146, }, Object { "x": 1607437620000, - "y": 1097.13333333333, + "y": 775.857142857143, }, ], - "throughput": Array [ + "value": 1135.15508885299, + }, + "name": "redis", + "spanSubtype": "redis", + "spanType": "db", + "throughput": Object { + "timeseries": Array [ Object { "x": 1607436720000, - "y": 1.06666666666667, + "y": 1.66666666666667, }, Object { "x": 1607436780000, - "y": 2.53333333333333, + "y": 2.73333333333333, }, Object { "x": 1607436840000, - "y": 2.73333333333333, + "y": 2.53333333333333, }, Object { "x": 1607436900000, - "y": 3.33333333333333, + "y": 3.13333333333333, }, Object { "x": 1607436960000, - "y": 2.66666666666667, + "y": 2.26666666666667, }, Object { "x": 1607437020000, - "y": 2.6, + "y": 2.86666666666667, }, Object { "x": 1607437080000, - "y": 2.66666666666667, + "y": 2.93333333333333, }, Object { "x": 1607437140000, - "y": 2.26666666666667, + "y": 2.66666666666667, }, Object { "x": 1607437200000, - "y": 2.93333333333333, + "y": 2.6, }, Object { "x": 1607437260000, @@ -394,28 +192,27 @@ Object { }, Object { "x": 1607437380000, - "y": 2.26666666666667, + "y": 2.4, }, Object { "x": 1607437440000, - "y": 2.86666666666667, + "y": 2.6, }, Object { "x": 1607437500000, - "y": 2.86666666666667, + "y": 2.66666666666667, }, Object { "x": 1607437560000, - "y": 2.6, + "y": 2.73333333333333, }, Object { "x": 1607437620000, - "y": 1, + "y": 1.86666666666667, }, ], + "value": 41.2666666666667, }, - "spanSubtype": "redis", - "spanType": "db", "type": "external", } `; @@ -423,223 +220,12 @@ Object { 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", - "currentPeriod": 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", - "previousPeriod": Object { - "errorRate": Array [ + "errorRate": Object { + "timeseries": Array [ Object { "x": 1607436720000, - "y": null, + "y": 0.333333333333333, }, Object { "x": 1607436780000, @@ -699,14 +285,17 @@ Object { }, Object { "x": 1607437620000, - "y": 0.333333333333333, + "y": 0, }, ], - "impact": 100, - "latency": Array [ + "value": 0.25, + }, + "impact": 100, + "latency": Object { + "timeseries": Array [ Object { "x": 1607436720000, - "y": null, + "y": 6.66666666666667, }, Object { "x": 1607436780000, @@ -766,13 +355,18 @@ Object { }, Object { "x": 1607437620000, - "y": 6.66666666666667, + "y": 1, }, ], - "throughput": Array [ + "value": 5.25, + }, + "name": "opbeans-node", + "serviceName": "opbeans-node", + "throughput": Object { + "timeseries": Array [ Object { "x": 1607436720000, - "y": null, + "y": 0.2, }, Object { "x": 1607436780000, @@ -832,11 +426,11 @@ Object { }, Object { "x": 1607437620000, - "y": 0.2, + "y": 0.0666666666666667, }, ], + "value": 0.266666666666667, }, - "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 394bdcedaa121..88488a62d4ebf 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 @@ -51,7 +51,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); expect(response.status).to.be(200); - expect(response.body.serviceDependencies).to.eql([]); + expect(response.body).to.eql({ currentPeriod: [], previousPeriod: {} }); }); } ); @@ -232,12 +232,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); }); - it('returns two dependencies', () => { - expect(response.body.serviceDependencies.length).to.be(2); + it('returns dependencies on current period', () => { + expect(response.body.currentPeriod.length).to.be(2); + }); + + it('returns dependencies on previous period', () => { + expect(Object.keys(response.body.previousPeriod).length).to.be(2); }); it('returns opbeans-node as a dependency', () => { - const opbeansNode = response.body.serviceDependencies.find( + const opbeansNode = response.body.currentPeriod.find( (item) => item.type === 'service' && item.serviceName === 'opbeans-node' ); @@ -245,57 +249,62 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(opbeansNode).toMatch(); - const currentPeriodFirstValue = roundNumber( - opbeansNode?.currentPeriod.latency.timeseries[0].y - ); - const currentPeriodLastValue = roundNumber( - last(opbeansNode?.currentPeriod.latency.timeseries)?.y - ); + const currentPeriodFirstValue = roundNumber(opbeansNode?.latency.timeseries[0].y); + const currentPeriodLastValue = roundNumber(last(opbeansNode?.latency.timeseries)?.y); expectSnapshot(currentPeriodFirstValue).toMatchInline(`"6.667"`); expectSnapshot(currentPeriodLastValue).toMatchInline(`"1.000"`); - - const previousPeriodFirstValue = roundNumber(opbeansNode?.previousPeriod.latency[0].y); - const previousPeriodLastValue = roundNumber(last(opbeansNode?.previousPeriod.latency)?.y); - - expectSnapshot(previousPeriodFirstValue).toMatchInline(`""`); - expectSnapshot(previousPeriodLastValue).toMatchInline(`"6.667"`); }); it('returns postgres as an external dependency', () => { - const postgres = response.body.serviceDependencies.find( + const postgresCurrentPeriod = response.body.currentPeriod.find( (item) => item.type === 'external' && item.name === 'postgres' ); - expect(postgres !== undefined).to.be(true); + const postgresPreviousPeriod = response.body.previousPeriod.postgres; - const values = { - currentPeriod: { - latency: roundNumber(postgres?.currentPeriod.latency.value), - throughput: roundNumber(postgres?.currentPeriod.throughput.value), - errorRate: roundNumber(postgres?.currentPeriod.errorRate.value), - impact: postgres?.currentPeriod.impact, - }, - previousPeriod: { - impact: postgres?.previousPeriod.impact, - }, - ...pick(postgres, 'spanType', 'spanSubtype', 'name', 'type'), + expect(postgresCurrentPeriod !== undefined).to.be(true); + expect(postgresPreviousPeriod !== undefined).to.be(true); + + const fields = ['spanType', 'spanSubtype', 'name', 'type']; + const currentValues = { + latency: roundNumber(postgresCurrentPeriod?.latency.value), + throughput: roundNumber(postgresCurrentPeriod?.throughput.value), + errorRate: roundNumber(postgresCurrentPeriod?.errorRate.value), + impact: postgresCurrentPeriod?.impact, + ...pick(postgresCurrentPeriod, ...fields), }; - expectSnapshot(values).toMatchInline(` + expectSnapshot(currentValues).toMatchInline(` Object { - "currentPeriod": Object { - "errorRate": "0.000", - "impact": 0, - "latency": "3.000", - "throughput": "0.06667", - }, + "errorRate": "0.000", + "impact": 0, + "latency": "3.000", "name": "postgres", - "previousPeriod": Object { - "impact": 0, - }, "spanSubtype": "http", "spanType": "external", + "throughput": "0.06667", + "type": "external", + } + `); + + const previousValues = { + latency: roundNumber(postgresPreviousPeriod?.latency.value), + throughput: roundNumber(postgresPreviousPeriod?.throughput.value), + errorRate: roundNumber(postgresPreviousPeriod?.errorRate.value), + impact: postgresPreviousPeriod?.impact, + ...pick(postgresPreviousPeriod, ...fields), + }; + + expectSnapshot(previousValues).toMatchInline(` + Object { + "errorRate": "0.000", + "impact": 0, + "latency": "3.000", + "name": "postgres", + "spanSubtype": "http", + "spanType": "external", + "throughput": "0.06667", "type": "external", } `); @@ -334,13 +343,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns at least one item', () => { - expect(response.body.serviceDependencies.length).to.be.greaterThan(0); - - expectSnapshot(response.body.serviceDependencies[0]).toMatch(); + expect(response.body.currentPeriod.length).to.be.greaterThan(0); + expectSnapshot(response.body.currentPeriod[0]).toMatch(); }); it('returns the right names', () => { - const names = response.body.serviceDependencies.map((item) => item.name); + const names = response.body.currentPeriod.map((item) => item.name); expectSnapshot(names.sort()).toMatchInline(` Array [ "elasticsearch", @@ -352,7 +360,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right service names', () => { - const serviceNames = response.body.serviceDependencies + const serviceNames = response.body.currentPeriod .map((item) => (item.type === 'service' ? item.serviceName : undefined)) .filter(Boolean); @@ -365,9 +373,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right latency values', () => { const latencyValues = sortBy( - response.body.serviceDependencies.map((item) => ({ + response.body.currentPeriod.map((item) => ({ name: item.name, - currentPeriodLatency: item.currentPeriod.latency.value, + currentPeriodLatency: item.latency.value, })), 'name' ); @@ -396,9 +404,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right throughput values', () => { const throughputValues = sortBy( - response.body.serviceDependencies.map((item) => ({ + response.body.currentPeriod.map((item) => ({ name: item.name, - currentPeriodThroughput: item.currentPeriod.throughput.value, + currentPeriodThroughput: item.throughput.value, })), 'name' ); @@ -427,15 +435,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right impact values', () => { const impactValues = sortBy( - response.body.serviceDependencies.map((item) => ({ + response.body.currentPeriod.map((item) => ({ name: item.name, currentPeriod: { - impact: item.currentPeriod.impact, - latency: item.currentPeriod.latency.value, - throughput: item.currentPeriod.throughput.value, - }, - previousPeriod: { - impact: item.previousPeriod.impact, + impact: item.impact, + latency: item.latency.value, + throughput: item.throughput.value, }, })), 'name' @@ -450,9 +455,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "throughput": 13.3333333333333, }, "name": "elasticsearch", - "previousPeriod": Object { - "impact": 1.19757368986118, - }, }, Object { "currentPeriod": Object { @@ -461,9 +463,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "throughput": 0.466666666666667, }, "name": "opbeans-java", - "previousPeriod": Object { - "impact": 0, - }, }, Object { "currentPeriod": Object { @@ -472,9 +471,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "throughput": 50.2666666666667, }, "name": "postgresql", - "previousPeriod": Object { - "impact": 100, - }, }, Object { "currentPeriod": Object { @@ -483,9 +479,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "throughput": 41.2666666666667, }, "name": "redis", - "previousPeriod": Object { - "impact": 1.59711836133489, - }, }, ] `); From 2edeb2503a6f97882a01cc370944640d24832ea0 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Wed, 14 Apr 2021 14:38:52 -0400 Subject: [PATCH 09/14] refactoring comparison --- .../find_common_connections.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts index 38f8f9577cc0b..375dfd29dd909 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { keyBy } from 'lodash'; import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; import { Connections } from './get_connections'; @@ -21,20 +22,15 @@ export function findCommonConnections({ currentPeriodConnections: Connections; previousPeriodConnections?: Connections; }) { - const currentPeriodConnectionsMap: Record< - string, - Connections - > = currentPeriodConnections.reduce((acc, curr) => { - if (curr.service?.name) { - const serviceName = curr.service.name; - return { ...acc, [serviceName]: curr }; - } - const destinationSource = curr[SPAN_DESTINATION_SERVICE_RESOURCE]; - if (destinationSource) { - return { ...acc, [destinationSource]: curr }; + const currentPeriodConnectionsMap = keyBy( + currentPeriodConnections, + (item) => { + const serviceName = item.service?.name; + return serviceName + ? serviceName + : item[SPAN_DESTINATION_SERVICE_RESOURCE]; } - return acc; - }, {}); + ); return previousPeriodConnections .map((item) => { From d383b389a4e31e2789d33b53f295841260865dbd Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 19 Apr 2021 18:36:49 -0400 Subject: [PATCH 10/14] addressing PR comments --- .../get_columns.tsx | 30 +- .../index.tsx | 14 +- .../find_common_connections.test.ts | 142 --- .../find_common_connections.ts | 50 -- .../get_connections.ts | 161 ---- .../get_destination_map.test.ts | 124 --- .../get_destination_map.ts | 247 ++++-- .../get_service_dependencies/helpers.test.ts | 819 ++++++++++++++++++ .../get_service_dependencies/helpers.ts | 212 +++++ .../get_service_dependencies/index.ts | 392 +++------ 10 files changed, 1382 insertions(+), 809 deletions(-) delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts delete mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.test.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.ts 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 index 373f4c3bd8a24..5c01e379e5179 100644 --- 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 @@ -26,13 +26,11 @@ type ServiceDependencies = APIReturnType<'GET /api/apm/services/{serviceName}/de export function getColumns({ environment, - previousPeriod, comparisonEnabled, }: { environment?: string; - previousPeriod: ServiceDependencies['previousPeriod']; comparisonEnabled?: boolean; -}): Array> { +}): Array> { return [ { field: 'name', @@ -88,12 +86,14 @@ export function getColumns({ width: px(unit * 10), render: (_, item) => { const previousPeriodLatencyTimeseries = - previousPeriod?.[item.name]?.latency.timeseries; + item.previousPeriodMetrics?.latency?.timeseries; return ( { const previousPeriodThroughputTimeseries = - previousPeriod?.[item.name]?.throughput.timeseries; + item.previousPeriodMetrics?.throughput?.timeseries; return ( { const previousPeriodErrorRateTimeseries = - previousPeriod?.[item.name]?.errorRate.timeseries; + item.previousPeriodMetrics?.errorRate?.timeseries; return ( { - const previousPeriodImpact = previousPeriod?.[item.name]?.impact || 0; + const previousPeriodImpact = item.previousPeriodMetrics?.impact || 0; return ( - + {comparisonEnabled && ( 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 920fa4b1503ba..0c3cfd5e30aa9 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 @@ -29,8 +29,7 @@ interface Props { } const INITIAL_STATE = { - currentPeriod: [], - previousPeriod: {}, + serviceDependencies: [], } as ServiceDependencies; export function ServiceOverviewDependenciesTable({ serviceName }: Props) { @@ -70,18 +69,17 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { ); // need top-level sortable fields for the managed table - const items = data.currentPeriod.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, - previousPeriod: data.previousPeriod, }); return ( diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts deleted file mode 100644 index 4f6bb8ab80fd2..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.test.ts +++ /dev/null @@ -1,142 +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 { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; -import { findCommonConnections } from './find_common_connections'; -import { Connections } from './get_connections'; - -describe('findCommonConnections', () => { - it("doesn't return connections when current and previous are empty", () => { - const currentPeriodConnections = [] as Connections; - const previousPeriodConnections = [] as Connections; - - expect( - findCommonConnections({ - currentPeriodConnections, - previousPeriodConnections, - }) - ).toEqual([]); - }); - it("doesn't return connections when current is empty", () => { - const currentPeriodConnections = [] as Connections; - const previousPeriodConnections = [ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-ruby' }, - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-java' }, - }, - ] as Connections; - - expect( - findCommonConnections({ - currentPeriodConnections, - previousPeriodConnections, - }) - ).toEqual([]); - }); - it("doesn't return connections when there are no match", () => { - const currentPeriodConnections = [ - { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-python' }, - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-dotnet' }, - }, - ] as Connections; - const previousPeriodConnections = [ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-ruby' }, - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-java' }, - }, - ] as Connections; - - expect( - findCommonConnections({ - currentPeriodConnections, - previousPeriodConnections, - }) - ).toEqual([]); - }); - it('returns connections without service name', () => { - const currentPeriodConnections = [ - { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-python' }, - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-dotnet' }, - }, - ] as Connections; - const previousPeriodConnections = [ - { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-ruby' }, - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-java' }, - }, - ] as Connections; - - expect( - findCommonConnections({ - currentPeriodConnections, - previousPeriodConnections, - }) - ).toEqual([{ [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }]); - }); - it('returns common connections', () => { - const currentPeriodConnections = [ - { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-python' }, - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-dotnet' }, - }, - ] as Connections; - const previousPeriodConnections = [ - { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-ruby' }, - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-dotnet' }, - }, - ] as Connections; - - expect( - findCommonConnections({ - currentPeriodConnections, - previousPeriodConnections, - }) - ).toEqual([ - { [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql' }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - service: { name: 'opbeans-dotnet' }, - }, - ]); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts deleted file mode 100644 index 375dfd29dd909..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/find_common_connections.ts +++ /dev/null @@ -1,50 +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 { keyBy } from 'lodash'; -import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; -import { Connections } from './get_connections'; - -function removeUndefined( - item: Connections[0] | undefined -): item is Connections[0] { - return !!item; -} - -export function findCommonConnections({ - currentPeriodConnections, - previousPeriodConnections = [], -}: { - currentPeriodConnections: Connections; - previousPeriodConnections?: Connections; -}) { - const currentPeriodConnectionsMap = keyBy( - currentPeriodConnections, - (item) => { - const serviceName = item.service?.name; - return serviceName - ? serviceName - : item[SPAN_DESTINATION_SERVICE_RESOURCE]; - } - ); - - return previousPeriodConnections - .map((item) => { - const serviceName = item.service?.name; - const destinationServiceResource = - item['span.destination.service.resource']; - - if ( - (serviceName && currentPeriodConnectionsMap[serviceName]) || - (destinationServiceResource && - currentPeriodConnectionsMap[destinationServiceResource]) - ) { - return item; - } - }) - .filter(removeUndefined); -} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts deleted file mode 100644 index a735e392560a4..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_connections.ts +++ /dev/null @@ -1,161 +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 { PromiseReturnType } from '../../../../../observability/typings/common'; -import { - AGENT_NAME, - EVENT_OUTCOME, - PARENT_ID, - SERVICE_ENVIRONMENT, - SERVICE_NAME, - SPAN_DESTINATION_SERVICE_RESOURCE, - SPAN_ID, - SPAN_SUBTYPE, - SPAN_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { asMutableArray } from '../../../../common/utils/as_mutable_array'; -import { joinByKey } from '../../../../common/utils/join_by_key'; -import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { withApmSpan } from '../../../utils/with_apm_span'; -import { Setup } from '../../helpers/setup_request'; - -export type Connections = PromiseReturnType; - -export const getConnections = ({ - setup, - serviceName, - environment, - start, - end, -}: { - setup: Setup; - serviceName: string; - environment?: string; - start: number; - end: number; -}) => { - return withApmSpan('get_service_destination_map', async () => { - const { apmEventClient } = setup; - - const response = await withApmSpan('get_exit_span_samples', async () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.span], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ], - }, - }, - aggs: { - connections: { - composite: { - size: 1000, - sources: asMutableArray([ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: { - terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, - }, - }, - // make sure we get samples for both successful - // and failed calls - { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, - ] as const), - }, - aggs: { - sample: { - top_hits: { - size: 1, - _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], - sort: [ - { - '@timestamp': 'desc' as const, - }, - ], - }, - }, - }, - }, - }, - }, - }) - ); - - const outgoingConnections = - response.aggregations?.connections.buckets.map((bucket) => { - const sample = bucket.sample.hits.hits[0]._source; - - return { - [SPAN_DESTINATION_SERVICE_RESOURCE]: String( - bucket.key[SPAN_DESTINATION_SERVICE_RESOURCE] - ), - [SPAN_ID]: sample.span.id, - [SPAN_TYPE]: sample.span.type, - [SPAN_SUBTYPE]: sample.span.subtype, - }; - }) ?? []; - - const transactionResponse = await withApmSpan( - 'get_transactions_for_exit_spans', - () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - query: { - bool: { - filter: [ - { - terms: { - [PARENT_ID]: outgoingConnections.map( - (connection) => connection[SPAN_ID] - ), - }, - }, - ...rangeQuery(start, end), - ], - }, - }, - size: outgoingConnections.length, - docvalue_fields: asMutableArray([ - SERVICE_NAME, - SERVICE_ENVIRONMENT, - AGENT_NAME, - PARENT_ID, - ] as const), - _source: false, - }, - }) - ); - - const incomingConnections = transactionResponse.hits.hits.map((hit) => ({ - [SPAN_ID]: String(hit.fields[PARENT_ID]![0]), - service: { - name: String(hit.fields[SERVICE_NAME]![0]), - environment: String(hit.fields[SERVICE_ENVIRONMENT]?.[0] ?? ''), - agentName: hit.fields[AGENT_NAME]![0] as AgentName, - }, - })); - - // merge outgoing spans with transactions by span.id/parent.id - const joinedBySpanId = joinByKey( - [...outgoingConnections, ...incomingConnections], - SPAN_ID - ); - return joinedBySpanId; - }); -}; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts deleted file mode 100644 index 56db44d0a751f..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.test.ts +++ /dev/null @@ -1,124 +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 { - SPAN_DESTINATION_SERVICE_RESOURCE, - SPAN_ID, - SPAN_SUBTYPE, - SPAN_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { Connections } from './get_connections'; -import { getDestinationMaps } from './get_destination_map'; - -describe('getDestintionMaps', () => { - it('returns empty when connections are empty', () => { - expect(getDestinationMaps([])).toEqual({}); - }); - - it('returns the destination for on connection without service', () => { - const connections = [ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql', - [SPAN_ID]: '7098b4957bd11904', - [SPAN_TYPE]: 'db', - [SPAN_SUBTYPE]: 'postgresql', - }, - ]; - expect(getDestinationMaps(connections)).toEqual({ - postgresql: { - id: { 'span.destination.service.resource': 'postgresql' }, - span: { - type: 'db', - subtype: 'postgresql', - destination: { service: { resource: 'postgresql' } }, - }, - }, - }); - }); - - it('returns the destination for on connection with service', () => { - const connections = [ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - [SPAN_ID]: 'b3c74623ee3951aa', - [SPAN_TYPE]: 'ext', - [SPAN_SUBTYPE]: 'http_rb', - service: { - name: 'opbeans-python', - environment: 'production', - agentName: 'python', - }, - }, - ] as Connections; - expect(getDestinationMaps(connections)).toEqual({ - 'opbeans:3000': { - id: { - service: { - name: 'opbeans-python', - environment: 'production', - agentName: 'python', - }, - }, - span: { - type: 'ext', - subtype: 'http_rb', - destination: { service: { resource: 'opbeans:3000' } }, - }, - service: { name: 'opbeans-python', environment: 'production' }, - agent: { name: 'python' }, - }, - }); - }); - - it('return the destination for multiples connections', () => { - const connections = [ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'postgresql', - [SPAN_ID]: '7098b4957bd11904', - [SPAN_TYPE]: 'db', - [SPAN_SUBTYPE]: 'postgresql', - }, - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans:3000', - [SPAN_ID]: 'e059b19db88ad19a', - [SPAN_TYPE]: 'ext', - [SPAN_SUBTYPE]: 'http_rb', - service: { - name: 'opbeans-dotnet', - environment: 'production', - agentName: 'dotnet', - }, - }, - ] as Connections; - - expect(getDestinationMaps(connections)).toEqual({ - postgresql: { - id: { 'span.destination.service.resource': 'postgresql' }, - span: { - type: 'db', - subtype: 'postgresql', - destination: { service: { resource: 'postgresql' } }, - }, - }, - 'opbeans:3000': { - id: { - service: { - name: 'opbeans-dotnet', - environment: 'production', - agentName: 'dotnet', - }, - }, - span: { - type: 'ext', - subtype: 'http_rb', - destination: { service: { resource: 'opbeans:3000' } }, - }, - service: { name: 'opbeans-dotnet', environment: 'production' }, - agent: { name: 'dotnet' }, - }, - }); - }); -}); 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 56ad513d5e2c8..db491012c986b 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 @@ -6,71 +6,210 @@ */ 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 { + AGENT_NAME, + EVENT_OUTCOME, + PARENT_ID, + SERVICE_ENVIRONMENT, + SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_ID, SPAN_SUBTYPE, SPAN_TYPE, } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; import { joinByKey } from '../../../../common/utils/join_by_key'; -import { Connections } from './get_connections'; - -export function getDestinationMaps(connections: Connections) { - // we could have multiple connections per address because of multiple event outcomes - const dedupedConnectionsByAddress = joinByKey( - connections, - SPAN_DESTINATION_SERVICE_RESOURCE - ); - - // identify a connection by either service.name, service.environment, agent.name - // OR span.destination.service.resource - const connectionsWithId = dedupedConnectionsByAddress.map((connection) => { - const id = - 'service' in connection - ? { service: connection.service } - : pickKeys(connection, SPAN_DESTINATION_SERVICE_RESOURCE); - - return { - ...connection, - id, - }; - }); +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export const getDestinationMap = ({ + setup, + serviceName, + environment, +}: { + setup: Setup & SetupTimeRange; + serviceName: string; + environment?: string; +}) => { + return withApmSpan('get_service_destination_map', async () => { + const { start, end, apmEventClient } = setup; - const dedupedConnectionsById = joinByKey(connectionsWithId, 'id'); - - const connectionsByAddress = keyBy( - connectionsWithId, - SPAN_DESTINATION_SERVICE_RESOURCE - ); - - // per span.destination.service.resource, return merged/deduped item - return mapValues(connectionsByAddress, ({ id }) => { - const connection = dedupedConnectionsById.find((dedupedConnection) => - isEqual(id, dedupedConnection.id) - )!; - - return { - id, - span: { - type: connection[SPAN_TYPE], - subtype: connection[SPAN_SUBTYPE], - destination: { - service: { - resource: connection[SPAN_DESTINATION_SERVICE_RESOURCE], + const response = await withApmSpan('get_exit_span_samples', async () => + apmEventClient.search({ + apm: { + events: [ProcessorEvent.span], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ], + }, + }, + aggs: { + connections: { + composite: { + size: 1000, + sources: asMutableArray([ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: { + terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, + }, + }, + // make sure we get samples for both successful + // and failed calls + { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, + ] as const), + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], + sort: [ + { + '@timestamp': 'desc' as const, + }, + ], + }, + }, + }, + }, }, }, + }) + ); + + const outgoingConnections = + response.aggregations?.connections.buckets.map((bucket) => { + const sample = bucket.sample.hits.hits[0]._source; + + return { + [SPAN_DESTINATION_SERVICE_RESOURCE]: String( + bucket.key[SPAN_DESTINATION_SERVICE_RESOURCE] + ), + [SPAN_ID]: sample.span.id, + [SPAN_TYPE]: sample.span.type, + [SPAN_SUBTYPE]: sample.span.subtype, + }; + }) ?? []; + + const transactionResponse = await withApmSpan( + 'get_transactions_for_exit_spans', + () => + apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + { + terms: { + [PARENT_ID]: outgoingConnections.map( + (connection) => connection[SPAN_ID] + ), + }, + }, + ...rangeQuery(start, end), + ], + }, + }, + size: outgoingConnections.length, + docvalue_fields: asMutableArray([ + SERVICE_NAME, + SERVICE_ENVIRONMENT, + AGENT_NAME, + PARENT_ID, + ] as const), + _source: false, + }, + }) + ); + + const incomingConnections = transactionResponse.hits.hits.map((hit) => ({ + [SPAN_ID]: String(hit.fields[PARENT_ID]![0]), + service: { + name: String(hit.fields[SERVICE_NAME]![0]), + environment: String(hit.fields[SERVICE_ENVIRONMENT]?.[0] ?? ''), + agentName: hit.fields[AGENT_NAME]![0] as AgentName, }, - ...('service' in connection && connection.service - ? { + })); + + // merge outgoing spans with transactions by span.id/parent.id + const joinedBySpanId = joinByKey( + [...outgoingConnections, ...incomingConnections], + SPAN_ID + ); + + // we could have multiple connections per address because + // of multiple event outcomes + const dedupedConnectionsByAddress = joinByKey( + joinedBySpanId, + SPAN_DESTINATION_SERVICE_RESOURCE + ); + + // identify a connection by either service.name, service.environment, agent.name + // OR span.destination.service.resource + + const connectionsWithId = dedupedConnectionsByAddress.map((connection) => { + const id = + 'service' in connection + ? { service: connection.service } + : pickKeys(connection, SPAN_DESTINATION_SERVICE_RESOURCE); + + return { + ...connection, + id, + }; + }); + + const dedupedConnectionsById = joinByKey(connectionsWithId, 'id'); + + const connectionsByAddress = keyBy( + connectionsWithId, + SPAN_DESTINATION_SERVICE_RESOURCE + ); + + // per span.destination.service.resource, return merged/deduped item + return mapValues(connectionsByAddress, ({ id }) => { + const connection = dedupedConnectionsById.find((dedupedConnection) => + isEqual(id, dedupedConnection.id) + )!; + + return { + id, + span: { + type: connection[SPAN_TYPE], + subtype: connection[SPAN_SUBTYPE], + destination: { service: { - name: connection.service.name, - environment: connection.service.environment, + resource: connection[SPAN_DESTINATION_SERVICE_RESOURCE], }, - agent: { - name: connection.service.agentName, - }, - } - : {}), - }; + }, + }, + ...('service' in connection && connection.service + ? { + service: { + name: connection.service.name, + environment: connection.service.environment, + }, + agent: { + name: connection.service.agentName, + }, + } + : {}), + }; + }); }); -} +}; 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..3697cb50a591a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.test.ts @@ -0,0 +1,819 @@ +/* + * 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. + */ + +// ### caue {"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"}}} +// ### caue [{"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},{"x":1618858500000,"count":17,"latency_sum":19376,"error_count":0},{"x":1618858560000,"count":6,"latency_sum":7994,"error_count":0},{"x":1618858620000,"count":6,"latency_sum":5662,"error_count":0},{"x":1618858680000,"count":18,"latency_sum":15776,"error_count":0},{"x":1618858740000,"count":12,"latency_sum":12960,"error_count":0},{"x":1618858800000,"count":6,"latency_sum":6516,"error_count":0},{"x":1618858860000,"count":21,"latency_sum":24850,"error_count":0},{"x":1618858920000,"count":8,"latency_sum":8579,"error_count":0},{"x":1618858980000,"count":17,"latency_sum":19964,"error_count":0},{"x":1618859040000,"count":16,"latency_sum":22130,"error_count":0},{"x":1618859100000,"count":10,"latency_sum":29056,"error_count":0},{"x":1618859160000,"count":9,"latency_sum":20861,"error_count":0},{"x":1618859220000,"count":18,"latency_sum":62265,"error_count":0},{"x":1618859280000,"count":0,"latency_sum":0,"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},{"x":1618858500000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858560000,"count":4,"latency_sum":4962889,"error_count":0},{"x":1618858620000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858680000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858740000,"count":1,"latency_sum":89422,"error_count":0},{"x":1618858800000,"count":2,"latency_sum":51248,"error_count":0},{"x":1618858860000,"count":1,"latency_sum":23162,"error_count":0},{"x":1618858920000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858980000,"count":2,"latency_sum":74924,"error_count":0},{"x":1618859040000,"count":1,"latency_sum":51224,"error_count":0},{"x":1618859100000,"count":1,"latency_sum":8004,"error_count":0},{"x":1618859160000,"count":1,"latency_sum":71817,"error_count":0},{"x":1618859220000,"count":3,"latency_sum":148184,"error_count":0},{"x":1618859280000,"count":0,"latency_sum":0,"error_count":0}]}] +// ### caue [{"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},{"x":1618772100000,"count":9,"latency_sum":21273,"error_count":0},{"x":1618772160000,"count":12,"latency_sum":25125,"error_count":0},{"x":1618772220000,"count":15,"latency_sum":31983,"error_count":0},{"x":1618772280000,"count":5,"latency_sum":14662,"error_count":0},{"x":1618772340000,"count":9,"latency_sum":10667,"error_count":0},{"x":1618772400000,"count":6,"latency_sum":12560,"error_count":0},{"x":1618772460000,"count":17,"latency_sum":86784,"error_count":0},{"x":1618772520000,"count":4,"latency_sum":15495,"error_count":0},{"x":1618772580000,"count":8,"latency_sum":13187,"error_count":0},{"x":1618772640000,"count":14,"latency_sum":37652,"error_count":0},{"x":1618772700000,"count":29,"latency_sum":59045,"error_count":0},{"x":1618772760000,"count":19,"latency_sum":53935,"error_count":0},{"x":1618772820000,"count":20,"latency_sum":45665,"error_count":0},{"x":1618772880000,"count":0,"latency_sum":0,"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},{"x":1618772100000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772160000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772220000,"count":3,"latency_sum":49008,"error_count":0},{"x":1618772280000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772340000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772400000,"count":3,"latency_sum":24475,"error_count":0},{"x":1618772460000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772520000,"count":1,"latency_sum":5577836,"error_count":0},{"x":1618772580000,"count":1,"latency_sum":15752,"error_count":0},{"x":1618772640000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772700000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772760000,"count":3,"latency_sum":47884,"error_count":0},{"x":1618772820000,"count":2,"latency_sum":30167,"error_count":0},{"x":1618772880000,"count":0,"latency_sum":0,"error_count":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..5f2d2ff56f19d --- /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 { getDestinationMap } 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: PromiseReturnType; + 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 6d6447d13490a..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,195 +5,33 @@ * 2.0. */ -import { keyBy, merge } from 'lodash'; -import { ValuesType } from 'utility-types'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; -import { joinByKey } from '../../../../common/utils/join_by_key'; -import { maybe } from '../../../../common/utils/maybe'; -import { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate'; import { withApmSpan } from '../../../utils/with_apm_span'; -import { calculateThroughput } from '../../helpers/calculate_throughput'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { findCommonConnections } from './find_common_connections'; -import { getConnections } from './get_connections'; -import { getDestinationMaps } from './get_destination_map'; +import { getDestinationMap } from './get_destination_map'; import { getMetrics } from './get_metrics'; - -function calculateMetrics({ - metrics, - destinationMap, - start, - end, -}: { - metrics: PromiseReturnType; - destinationMap: ReturnType; - start: number; - end: number; -}) { - const metricsWithDestinationIds = metrics.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 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 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, - })), - }, - }; - - if (item.service) { - return { - name: item.service.name, - type: 'service' as const, - serviceName: item.service.name, - environment: item.service.environment, - // agent.name should always be there, type returned from joinByKey is too pessimistic - agentName: item.agent!.name, - ...destMetrics, - }; - } - - return { - name: item.span.destination.service.resource, - type: 'external' as const, - spanType: item.span.type, - spanSubtype: item.span.subtype, - ...destMetrics, - }; - }); - - 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); - - 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; - - return { - ...metric, - impact, - }; - }); -} - -async function getServiceMetricsAndConnections({ +import { + calculateDestinationMetrics, + getCalculateImpact, + getMetricsWithDestinationIds, + joinMetricsByDestinationId, + calculateMetricValues, + offsetPreviousMetrics, +} from './helpers'; + +async function getServiceMetricsAndDestinationMap({ setup, serviceName, environment, numBuckets, - start, - end, }: { serviceName: string; - setup: Setup; + setup: Setup & SetupTimeRange; environment?: string; numBuckets: number; - start: number; - end: number; }) { - return await Promise.all([ + const { start, end } = setup; + const [metrics, destinationMap] = await Promise.all([ getMetrics({ setup, serviceName, @@ -202,14 +40,13 @@ async function getServiceMetricsAndConnections({ start, end, }), - getConnections({ + getDestinationMap({ setup, serviceName, environment, - start, - end, }), ]); + return { metrics, destinationMap }; } export async function getServiceDependenciesPerPeriod({ @@ -230,90 +67,133 @@ export async function getServiceDependenciesPerPeriod({ return withApmSpan('get_service_dependencies', async () => { const { start, end } = setup; - const [ - [currentPeriodMetrics, currentPeriodConnections], - [previousPeriodMetrics, previousPeriodConnections] = [], - ] = await Promise.all([ - getServiceMetricsAndConnections({ - setup, - serviceName, - environment, - numBuckets, - start, - end, - }), - ...(comparisonStart && comparisonEnd - ? [ - getServiceMetricsAndConnections({ - setup, - serviceName, - environment, - numBuckets, - start: comparisonStart, - end: comparisonEnd, - }), - ] - : []), - ]); + const currentPeriodPromise = getServiceMetricsAndDestinationMap({ + setup, + serviceName, + environment, + numBuckets, + }); - const commonPreviousConnections = findCommonConnections({ - currentPeriodConnections, - previousPeriodConnections, + 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 currentDestinationMap = getDestinationMaps(currentPeriodConnections); - const previousDestinationMap = getDestinationMaps( - commonPreviousConnections + const metricsJoinedByDestinationId = joinMetricsByDestinationId( + metricsWithDestinationIds ); - const currentPeriod = calculateMetrics({ - metrics: currentPeriodMetrics, - destinationMap: currentDestinationMap, - start, - end, - }); + const metricsByResolvedAddress = metricsJoinedByDestinationId.map( + (item) => { + const { metrics, previousMetrics } = item; + const mergedMetrics = calculateMetricValues(metrics); + const previousPeriodMergedMetrics = calculateMetricValues( + previousMetrics + ); + + 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 { + name: item.service.name, + type: 'service' as const, + serviceName: item.service.name, + environment: item.service.environment, + // agent.name should always be there, type returned from joinByKey is too pessimistic + agentName: item.agent!.name, + currentPeriodMetrics: currentDestinationMetrics, + previousPeriodMetrics: offsetPreviousDestinationMetrics, + }; + } - const currentPeriodMap = keyBy(currentPeriod, 'name'); + return { + name: item.span.destination.service.resource, + type: 'external' as const, + spanType: item.span.type, + spanSubtype: item.span.subtype, + currentPeriodMetrics: currentDestinationMetrics, + previousPeriodMetrics: offsetPreviousDestinationMetrics, + }; + } + ); - const previousPeriod = - comparisonStart && comparisonEnd && previousPeriodMetrics - ? calculateMetrics({ - metrics: previousPeriodMetrics, - destinationMap: previousDestinationMap, - start: comparisonStart, - end: comparisonEnd, - }).map((previousItem) => { - const currentItem = currentPeriodMap[previousItem.name]; - return { - ...previousItem, - latency: { - ...previousItem.latency, - timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentItem?.latency.timeseries, - previousPeriodTimeseries: previousItem.latency.timeseries, - }), - }, - throughput: { - ...previousItem.throughput, - timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentItem?.throughput.timeseries, - previousPeriodTimeseries: previousItem.throughput.timeseries, - }), - }, - errorRate: { - ...previousItem.errorRate, - timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentItem?.errorRate.timeseries, - previousPeriodTimeseries: previousItem.errorRate.timeseries, - }), - }, - }; - }) - : []; + const calculateCurrentImpact = getCalculateImpact( + metricsByResolvedAddress + .map( + (metric) => + (metric.currentPeriodMetrics.latency.value ?? 0) * + (metric.currentPeriodMetrics.throughput.value ?? 0) + ) + .filter(isFiniteNumber) + ); + + 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, + 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 { - currentPeriod, - previousPeriod: keyBy(previousPeriod, 'name'), - }; + return { serviceDependencies }; }); } From 0a08d5be8453827bbeb80d8606baf10b57b1aaf5 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 19 Apr 2021 18:54:56 -0400 Subject: [PATCH 11/14] fixing api test --- .../dependencies/__snapshots__/index.snap | 1254 +++++++++++------ .../service_overview/dependencies/index.ts | 112 +- 2 files changed, 891 insertions(+), 475 deletions(-) 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 index 6219c8d3857db..dcd6e84dcfa0f 100644 --- 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 @@ -2,217 +2,429 @@ exports[`APM API tests basic apm_8.0.0 Service overview dependencies when data is loaded returns at least one item 1`] = ` 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, + "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", - "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, - }, "type": "external", } `; @@ -220,217 +432,429 @@ Object { 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", - "environment": "", - "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, + "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", - "serviceName": "opbeans-node", - "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, + "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 88488a62d4ebf..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 @@ -51,7 +51,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); expect(response.status).to.be(200); - expect(response.body).to.eql({ currentPeriod: [], previousPeriod: {} }); + expect(response.body).to.eql({ serviceDependencies: [] }); }); } ); @@ -232,16 +232,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); }); - it('returns dependencies on current period', () => { - expect(response.body.currentPeriod.length).to.be(2); - }); - - it('returns dependencies on previous period', () => { - expect(Object.keys(response.body.previousPeriod).length).to.be(2); + it('returns correct number of dependencies', () => { + expect(response.body.serviceDependencies.length).to.be(2); }); it('returns opbeans-node as a dependency', () => { - const opbeansNode = response.body.currentPeriod.find( + const opbeansNode = response.body.serviceDependencies.find( (item) => item.type === 'service' && item.serviceName === 'opbeans-node' ); @@ -249,30 +245,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(opbeansNode).toMatch(); - const currentPeriodFirstValue = roundNumber(opbeansNode?.latency.timeseries[0].y); - const currentPeriodLastValue = roundNumber(last(opbeansNode?.latency.timeseries)?.y); + const currentPeriodFirstValue = roundNumber( + opbeansNode?.currentPeriodMetrics.latency.timeseries[0].y + ); + const currentPeriodLastValue = roundNumber( + last(opbeansNode?.previousPeriodMetrics?.latency?.timeseries)?.y + ); expectSnapshot(currentPeriodFirstValue).toMatchInline(`"6.667"`); - expectSnapshot(currentPeriodLastValue).toMatchInline(`"1.000"`); + expectSnapshot(currentPeriodLastValue).toMatchInline(`"6.667"`); }); it('returns postgres as an external dependency', () => { - const postgresCurrentPeriod = response.body.currentPeriod.find( + const postgres = response.body.serviceDependencies.find( (item) => item.type === 'external' && item.name === 'postgres' ); - const postgresPreviousPeriod = response.body.previousPeriod.postgres; - - expect(postgresCurrentPeriod !== undefined).to.be(true); - expect(postgresPreviousPeriod !== undefined).to.be(true); + expect(postgres !== undefined).to.be(true); const fields = ['spanType', 'spanSubtype', 'name', 'type']; const currentValues = { - latency: roundNumber(postgresCurrentPeriod?.latency.value), - throughput: roundNumber(postgresCurrentPeriod?.throughput.value), - errorRate: roundNumber(postgresCurrentPeriod?.errorRate.value), - impact: postgresCurrentPeriod?.impact, - ...pick(postgresCurrentPeriod, ...fields), + 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), }; expectSnapshot(currentValues).toMatchInline(` @@ -289,11 +286,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { `); const previousValues = { - latency: roundNumber(postgresPreviousPeriod?.latency.value), - throughput: roundNumber(postgresPreviousPeriod?.throughput.value), - errorRate: roundNumber(postgresPreviousPeriod?.errorRate.value), - impact: postgresPreviousPeriod?.impact, - ...pick(postgresPreviousPeriod, ...fields), + 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(` @@ -343,12 +340,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns at least one item', () => { - expect(response.body.currentPeriod.length).to.be.greaterThan(0); - expectSnapshot(response.body.currentPeriod[0]).toMatch(); + expect(response.body.serviceDependencies.length).to.be.greaterThan(0); + expectSnapshot(response.body.serviceDependencies[0]).toMatch(); }); it('returns the right names', () => { - const names = response.body.currentPeriod.map((item) => item.name); + const names = response.body.serviceDependencies.map((item) => item.name); expectSnapshot(names.sort()).toMatchInline(` Array [ "elasticsearch", @@ -360,7 +357,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right service names', () => { - const serviceNames = response.body.currentPeriod + const serviceNames = response.body.serviceDependencies .map((item) => (item.type === 'service' ? item.serviceName : undefined)) .filter(Boolean); @@ -373,9 +370,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right latency values', () => { const latencyValues = sortBy( - response.body.currentPeriod.map((item) => ({ + response.body.serviceDependencies.map((item) => ({ name: item.name, - currentPeriodLatency: item.latency.value, + currentPeriodLatency: item.currentPeriodMetrics.latency.value, + previousPeriodLatency: item.previousPeriodMetrics?.latency?.value, })), 'name' ); @@ -385,18 +383,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { Object { "currentPeriodLatency": 2628.905, "name": "elasticsearch", + "previousPeriodLatency": 2505.390625, }, Object { "currentPeriodLatency": 27859.2857142857, "name": "opbeans-java", + "previousPeriodLatency": 23831.8888888889, }, Object { "currentPeriodLatency": 28580.1312997347, "name": "postgresql", + "previousPeriodLatency": 29184.1857142857, }, Object { "currentPeriodLatency": 1135.15508885299, "name": "redis", + "previousPeriodLatency": 949.938333333333, }, ] `); @@ -404,9 +406,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right throughput values', () => { const throughputValues = sortBy( - response.body.currentPeriod.map((item) => ({ + response.body.serviceDependencies.map((item) => ({ name: item.name, - currentPeriodThroughput: item.throughput.value, + currentPeriodThroughput: item.currentPeriodMetrics.throughput.value, + previousPeriodThroughput: item.previousPeriodMetrics?.throughput?.value, })), 'name' ); @@ -416,18 +419,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { Object { "currentPeriodThroughput": 13.3333333333333, "name": "elasticsearch", + "previousPeriodThroughput": 12.8, }, Object { "currentPeriodThroughput": 0.466666666666667, "name": "opbeans-java", + "previousPeriodThroughput": 0.6, }, Object { "currentPeriodThroughput": 50.2666666666667, "name": "postgresql", + "previousPeriodThroughput": 51.3333333333333, }, Object { "currentPeriodThroughput": 41.2666666666667, "name": "redis", + "previousPeriodThroughput": 40, }, ] `); @@ -435,13 +442,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right impact values', () => { const impactValues = sortBy( - response.body.currentPeriod.map((item) => ({ + response.body.serviceDependencies.map((item) => ({ name: item.name, - currentPeriod: { - impact: item.impact, - latency: item.latency.value, - throughput: item.throughput.value, - }, + currentPeriodImpact: item.currentPeriodMetrics.impact, + previousPeriodImpact: item.previousPeriodMetrics?.impact, })), 'name' ); @@ -449,36 +453,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(impactValues).toMatchInline(` Array [ Object { - "currentPeriod": Object { - "impact": 1.54893576051104, - "latency": 2628.905, - "throughput": 13.3333333333333, - }, + "currentPeriodImpact": 1.54893576051104, "name": "elasticsearch", + "previousPeriodImpact": 1.19757368986118, }, Object { - "currentPeriod": Object { - "impact": 0, - "latency": 27859.2857142857, - "throughput": 0.466666666666667, - }, + "currentPeriodImpact": 0, "name": "opbeans-java", + "previousPeriodImpact": 0, }, Object { - "currentPeriod": Object { - "impact": 100, - "latency": 28580.1312997347, - "throughput": 50.2666666666667, - }, + "currentPeriodImpact": 100, "name": "postgresql", + "previousPeriodImpact": 100, }, Object { - "currentPeriod": Object { - "impact": 2.37724265214801, - "latency": 1135.15508885299, - "throughput": 41.2666666666667, - }, + "currentPeriodImpact": 2.37724265214801, "name": "redis", + "previousPeriodImpact": 1.59711836133489, }, ] `); From cedc8dea28471b8a60b238aa621015b65f3612e5 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 19 Apr 2021 19:06:53 -0400 Subject: [PATCH 12/14] stop fetching comparison data when it is disabled --- .../service_overview_dependencies_table/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0c3cfd5e30aa9..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 @@ -41,11 +41,12 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { start, end, comparisonType, + comparisonEnabled, }); const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { - if (!start || !end || !comparisonStart || !comparisonEnd) { + if (!start || !end) { return; } return callApmApi({ From 6588d4308d29694699fc6becdf9061709df972f1 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 20 Apr 2021 08:17:48 -0400 Subject: [PATCH 13/14] fixing unit test --- .../components/app/service_overview/service_overview.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 7e2d83fefefba..a91ed953fb790 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 @@ -97,8 +97,7 @@ describe('ServiceOverview', () => { isAggregationAccurate: true, }, 'GET /api/apm/services/{serviceName}/dependencies': { - currentPeriod: [], - previousPeriod: [], + serviceDependencies: [], }, 'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics': [], }; From a1acc7edebc5b2e440b6d3cd9270ccf3f2bc75a1 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 20 Apr 2021 13:02:45 -0400 Subject: [PATCH 14/14] fixing TS issue --- .../get_destination_map.ts | 29 +++++++++++++++++-- .../get_service_dependencies/helpers.test.ts | 4 --- .../get_service_dependencies/helpers.ts | 4 +-- 3 files changed, 29 insertions(+), 8 deletions(-) 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/helpers.test.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/helpers.test.ts index 3697cb50a591a..c8e287dadf06a 100644 --- 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 @@ -5,10 +5,6 @@ * 2.0. */ -// ### caue {"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"}}} -// ### caue [{"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},{"x":1618858500000,"count":17,"latency_sum":19376,"error_count":0},{"x":1618858560000,"count":6,"latency_sum":7994,"error_count":0},{"x":1618858620000,"count":6,"latency_sum":5662,"error_count":0},{"x":1618858680000,"count":18,"latency_sum":15776,"error_count":0},{"x":1618858740000,"count":12,"latency_sum":12960,"error_count":0},{"x":1618858800000,"count":6,"latency_sum":6516,"error_count":0},{"x":1618858860000,"count":21,"latency_sum":24850,"error_count":0},{"x":1618858920000,"count":8,"latency_sum":8579,"error_count":0},{"x":1618858980000,"count":17,"latency_sum":19964,"error_count":0},{"x":1618859040000,"count":16,"latency_sum":22130,"error_count":0},{"x":1618859100000,"count":10,"latency_sum":29056,"error_count":0},{"x":1618859160000,"count":9,"latency_sum":20861,"error_count":0},{"x":1618859220000,"count":18,"latency_sum":62265,"error_count":0},{"x":1618859280000,"count":0,"latency_sum":0,"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},{"x":1618858500000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858560000,"count":4,"latency_sum":4962889,"error_count":0},{"x":1618858620000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858680000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858740000,"count":1,"latency_sum":89422,"error_count":0},{"x":1618858800000,"count":2,"latency_sum":51248,"error_count":0},{"x":1618858860000,"count":1,"latency_sum":23162,"error_count":0},{"x":1618858920000,"count":0,"latency_sum":0,"error_count":0},{"x":1618858980000,"count":2,"latency_sum":74924,"error_count":0},{"x":1618859040000,"count":1,"latency_sum":51224,"error_count":0},{"x":1618859100000,"count":1,"latency_sum":8004,"error_count":0},{"x":1618859160000,"count":1,"latency_sum":71817,"error_count":0},{"x":1618859220000,"count":3,"latency_sum":148184,"error_count":0},{"x":1618859280000,"count":0,"latency_sum":0,"error_count":0}]}] -// ### caue [{"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},{"x":1618772100000,"count":9,"latency_sum":21273,"error_count":0},{"x":1618772160000,"count":12,"latency_sum":25125,"error_count":0},{"x":1618772220000,"count":15,"latency_sum":31983,"error_count":0},{"x":1618772280000,"count":5,"latency_sum":14662,"error_count":0},{"x":1618772340000,"count":9,"latency_sum":10667,"error_count":0},{"x":1618772400000,"count":6,"latency_sum":12560,"error_count":0},{"x":1618772460000,"count":17,"latency_sum":86784,"error_count":0},{"x":1618772520000,"count":4,"latency_sum":15495,"error_count":0},{"x":1618772580000,"count":8,"latency_sum":13187,"error_count":0},{"x":1618772640000,"count":14,"latency_sum":37652,"error_count":0},{"x":1618772700000,"count":29,"latency_sum":59045,"error_count":0},{"x":1618772760000,"count":19,"latency_sum":53935,"error_count":0},{"x":1618772820000,"count":20,"latency_sum":45665,"error_count":0},{"x":1618772880000,"count":0,"latency_sum":0,"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},{"x":1618772100000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772160000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772220000,"count":3,"latency_sum":49008,"error_count":0},{"x":1618772280000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772340000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772400000,"count":3,"latency_sum":24475,"error_count":0},{"x":1618772460000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772520000,"count":1,"latency_sum":5577836,"error_count":0},{"x":1618772580000,"count":1,"latency_sum":15752,"error_count":0},{"x":1618772640000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772700000,"count":0,"latency_sum":0,"error_count":0},{"x":1618772760000,"count":3,"latency_sum":47884,"error_count":0},{"x":1618772820000,"count":2,"latency_sum":30167,"error_count":0},{"x":1618772880000,"count":0,"latency_sum":0,"error_count":0}]}] - import { PromiseReturnType } from '../../../../../observability/typings/common'; import { getDestinationMap } from './get_destination_map'; import { getMetrics } from './get_metrics'; 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 index 5f2d2ff56f19d..44f0b77bede8c 100644 --- 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 @@ -12,7 +12,7 @@ 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 { getDestinationMap } from './get_destination_map'; +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'; @@ -22,7 +22,7 @@ export function getMetricsWithDestinationIds({ currentPeriodMetrics, previousPeriodMetrics, }: { - destinationMap: PromiseReturnType; + destinationMap: DestinationMap; currentPeriodMetrics: PromiseReturnType; previousPeriodMetrics: PromiseReturnType; }) {