From 5e14e4277dd367b560e79296eb414a12bf2c418d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 30 Jun 2020 19:49:15 +0200 Subject: [PATCH] use join utility function to merge requests --- .../common/utils/join_by_key/index.test.ts | 104 ++++++++++++++++++ .../apm/common/utils/join_by_key/index.ts | 48 ++++++++ x-pack/plugins/apm/common/utils/left_join.ts | 21 ---- .../get_services/get_services_items.ts | 69 ++++-------- ...metrics.ts => get_services_items_stats.ts} | 30 ++--- 5 files changed, 187 insertions(+), 85 deletions(-) create mode 100644 x-pack/plugins/apm/common/utils/join_by_key/index.test.ts create mode 100644 x-pack/plugins/apm/common/utils/join_by_key/index.ts delete mode 100644 x-pack/plugins/apm/common/utils/left_join.ts rename x-pack/plugins/apm/server/lib/services/get_services/{get_metrics.ts => get_services_items_stats.ts} (90%) diff --git a/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts b/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts new file mode 100644 index 0000000000000..458d21bfea58f --- /dev/null +++ b/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { joinByKey } from './'; + +describe('joinByKey', () => { + it('joins by a string key', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + avg: 10, + }, + { + serviceName: 'opbeans-node', + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + }, + { + serviceName: 'opbeans-java', + p95: 18, + }, + ], + 'serviceName' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + serviceName: 'opbeans-node', + avg: 10, + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + p95: 18, + }, + ]); + }); + + it('joins by a record key', () => { + const joined = joinByKey( + [ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + }, + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + p95: 18, + }, + ], + 'key' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + p95: 18, + }, + ]); + }); +}); diff --git a/x-pack/plugins/apm/common/utils/join_by_key/index.ts b/x-pack/plugins/apm/common/utils/join_by_key/index.ts new file mode 100644 index 0000000000000..b49f536400514 --- /dev/null +++ b/x-pack/plugins/apm/common/utils/join_by_key/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { UnionToIntersection, ValuesType } from 'utility-types'; +import { isEqual } from 'lodash'; + +/** + * Joins a list of records by a given key. Key can be any type of value, from + * strings to plain objects, as long as it is present in all records. `isEqual` + * is used for comparing keys. + * + * UnionToIntersection is needed to get all keys of union types, see below for + * example. + * + const agentNames = [{ serviceName: '', agentName: '' }]; + const transactionRates = [{ serviceName: '', transactionsPerMinute: 1 }]; + const flattened = joinByKey( + [...agentNames, ...transactionRates], + 'serviceName' + ); +*/ + +type JoinedReturnType< + T extends Record, + U extends UnionToIntersection, + V extends keyof T & keyof U +> = Array & Record>; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends keyof T & keyof U +>(items: T[], key: V): JoinedReturnType { + return items.reduce>((prev, current) => { + let item = prev.find((prevItem) => isEqual(prevItem[key], current[key])); + + if (!item) { + item = { ...current } as ValuesType>; + prev.push(item); + } else { + Object.assign(item, current); + } + + return prev; + }, []); +} diff --git a/x-pack/plugins/apm/common/utils/left_join.ts b/x-pack/plugins/apm/common/utils/left_join.ts deleted file mode 100644 index f3c4e48df755b..0000000000000 --- a/x-pack/plugins/apm/common/utils/left_join.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Assign, Omit } from 'utility-types'; - -export function leftJoin< - TL extends object, - K extends keyof TL, - TR extends Pick ->(leftRecords: TL[], matchKey: K, rightRecords: TR[]) { - const rightLookup = new Map( - rightRecords.map((record) => [record[matchKey], record]) - ); - return leftRecords.map((record) => { - const matchProp = (record[matchKey] as unknown) as TR[K]; - const matchingRightRecord = rightLookup.get(matchProp); - return { ...record, ...matchingRightRecord }; - }) as Array>>>; -} diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index f277ca7aae6bc..14772e77fe1c2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -3,8 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { uniq } from 'lodash'; -import { arrayUnionToCallable } from '../../../../common/utils/array_union_to_callable'; +import { joinByKey } from '../../../../common/utils/join_by_key'; import { PromiseReturnType } from '../../../../typings/common'; import { Setup, @@ -13,12 +12,12 @@ import { } from '../../helpers/setup_request'; import { getServicesProjection } from '../../../../common/projections/services'; import { - getTransactionDurationAvg, - getAgentName, - getTransactionRate, - getErrorRate, + getTransactionDurationAverages, + getAgentNames, + getTransactionRates, + getErrorRates, getEnvironments, -} from './get_metrics'; +} from './get_services_items_stats'; export type ServiceListAPIResponse = PromiseReturnType; export type ServicesItemsSetup = Setup & SetupTimeRange & SetupUIFilters; @@ -31,54 +30,26 @@ export async function getServicesItems(setup: ServicesItemsSetup) { }; const [ - transactionDurationAvg, - agentName, - transactionRate, - errorRate, + transactionDurationAverages, + agentNames, + transactionRates, + errorRates, environments, ] = await Promise.all([ - getTransactionDurationAvg(params), - getAgentName(params), - getTransactionRate(params), - getErrorRate(params), + getTransactionDurationAverages(params), + getAgentNames(params), + getTransactionRates(params), + getErrorRates(params), getEnvironments(params), ]); const allMetrics = [ - transactionDurationAvg, - agentName, - transactionRate, - errorRate, - environments, + ...transactionDurationAverages, + ...agentNames, + ...transactionRates, + ...errorRates, + ...environments, ]; - const serviceNames = uniq( - arrayUnionToCallable( - allMetrics.flatMap((metric) => - arrayUnionToCallable(metric).map((service) => service.name) - ) - ) - ); - - const items = serviceNames.map((serviceName) => { - return { - serviceName, - agentName: - agentName.find((service) => service.name === serviceName)?.value ?? - null, - transactionsPerMinute: - transactionRate.find((service) => service.name === serviceName) - ?.value ?? 0, - errorsPerMinute: - errorRate.find((service) => service.name === serviceName)?.value ?? 0, - avgResponseTime: - transactionDurationAvg.find((service) => service.name === serviceName) - ?.value ?? null, - environments: - environments.find((service) => service.name === serviceName)?.value ?? - [], - }; - }); - - return items; + return joinByKey(allMetrics, 'serviceName'); } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_metrics.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts similarity index 90% rename from x-pack/plugins/apm/server/lib/services/get_services/get_metrics.ts rename to x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index b950b84c4f64b..3f41fc95042d9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_metrics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -28,7 +28,7 @@ interface AggregationParams { projection: ServicesItemsProjection; } -export const getTransactionDurationAvg = async ({ +export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { @@ -74,12 +74,12 @@ export const getTransactionDurationAvg = async ({ } return aggregations.services.buckets.map((bucket) => ({ - name: bucket.key as string, - value: bucket.average.value, + serviceName: bucket.key as string, + avgResponseTime: bucket.average.value, })); }; -export const getAgentName = async ({ +export const getAgentNames = async ({ setup, projection, }: AggregationParams) => { @@ -136,8 +136,8 @@ export const getAgentName = async ({ } return aggregations.services.buckets.map((bucket) => ({ - name: bucket.key as string, - value: (bucket.agent_name.hits.hits[0]?._source as { + serviceName: bucket.key as string, + agentName: (bucket.agent_name.hits.hits[0]?._source as { agent: { name: string; }; @@ -145,7 +145,7 @@ export const getAgentName = async ({ })); }; -export const getTransactionRate = async ({ +export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { @@ -190,13 +190,13 @@ export const getTransactionRate = async ({ return arrayUnionToCallable(aggregations.services.buckets).map((bucket) => { const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; return { - name: bucket.key as string, - value: transactionsPerMinute, + serviceName: bucket.key as string, + transactionsPerMinute, }; }); }; -export const getErrorRate = async ({ +export const getErrorRates = async ({ setup, projection, }: AggregationParams) => { @@ -231,10 +231,10 @@ export const getErrorRate = async ({ const deltaAsMinutes = getDeltaAsMinutes(setup); return aggregations.services.buckets.map((bucket) => { - const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; + const errorsPerMinute = bucket.doc_count / deltaAsMinutes; return { - name: bucket.key as string, - value: transactionsPerMinute, + serviceName: bucket.key as string, + errorsPerMinute, }; }); }; @@ -295,7 +295,7 @@ export const getEnvironments = async ({ } return aggregations.services.buckets.map((bucket) => ({ - name: bucket.key as string, - value: bucket.environments.buckets.map((env) => env.key as string), + serviceName: bucket.key as string, + environments: bucket.environments.buckets.map((env) => env.key as string), })); };