From 4b05fd9f3105bd2eed169eaa0fd77b8f6b2889c5 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 15 Aug 2019 10:53:20 +0200 Subject: [PATCH] [APM] Local UI filters (#41588) * Add snapshot tests for query search params * Use projections to fetch data * Add configuration and aggregations for local filters * Add endpoints for local filters * UseLocalUIFilters hook * Parse Local UI filters from URL params * LocalUIFilters component * TransactionTypeFilter * replace useServiceTransactionTypes with useTransactionTypes * Support transactionType for trace projection/api * Configure Local UI Filters * Use units instead of hardcoded px * Fix i18n error * Remove unused labels from translation files * Update URL of API integration test for transaction_types API call * Revert "replace useServiceTransactionTypes with useTransactionTypes" This reverts commit bae7fa44a0fc1ace0b498cc78626fabb2d310437. * Revert "Support transactionType for trace projection/api" This reverts commit 2edb32cf04ca2c38885c457ba57ce0a1547fd199. * Remove transaction type filter for traces,error groups * Address review feedback * Don't clone in mergeProjection; add tests * Address review feedback * Revert transaction_types API path in functional tests * More review feedback * More review feedback: - Add transactions projection - Merge traces/transaction groups projections * Don't persist local UI filters in HistoryTabs * Move projections to server folder * Move transactionName into options * Use pod name instead of pod uid --- .../elasticsearch_fieldnames.test.ts.snap | 18 + .../apm/common/elasticsearch_fieldnames.ts | 4 + .../plugins/apm/common/projections/errors.ts | 46 ++ .../plugins/apm/common/projections/metrics.ts | 37 + .../apm/common/projections/services.ts | 42 ++ .../common/projections/transaction_groups.ts | 46 ++ .../apm/common/projections/transactions.ts | 58 ++ .../plugins/apm/common/projections/typings.ts | 28 + .../util/merge_projection/index.test.ts | 61 ++ .../util/merge_projection/index.ts | 32 + .../app/ErrorGroupOverview/index.tsx | 83 ++- .../app/ServiceDetails/ServiceDetailTabs.tsx | 2 +- .../components/app/ServiceMetrics/index.tsx | 61 +- .../__test__/ServiceOverview.test.tsx | 9 + .../components/app/ServiceOverview/index.tsx | 43 +- .../components/app/TraceOverview/index.tsx | 28 +- .../app/TransactionDetails/index.tsx | 97 ++- .../__jest__/TransactionOverview.test.tsx | 87 ++- .../TransactionOverview.test.tsx.snap | 19 - .../app/TransactionOverview/index.tsx | 121 ++-- .../components/shared/HistoryTabs/index.tsx | 8 +- .../components/shared/Links/url_helpers.ts | 5 +- .../LocalUIFilters/Filter/FilterBadgeList.tsx | 43 ++ .../Filter/FilterTitleButton.tsx | 35 + .../shared/LocalUIFilters/Filter/index.tsx | 173 +++++ .../TransactionTypeFilter/index.tsx | 64 ++ .../shared/LocalUIFilters/index.tsx | 88 +++ .../public/context/UrlParamsContext/index.tsx | 21 +- .../UrlParamsContext/resolveUrlParams.ts | 11 +- .../public/context/UrlParamsContext/types.ts | 6 +- .../apm/public/hooks/useLocalUIFilters.ts | 82 +++ .../plugins/apm/public/utils/pickKeys.ts | 10 + .../plugins/apm/public/utils/testHelpers.tsx | 52 ++ .../errors/__snapshots__/queries.test.ts.snap | 248 +++++++ .../__snapshots__/queries.test.ts.snap | 110 +++ .../lib/errors/distribution/queries.test.ts | 42 ++ .../apm/server/lib/errors/get_error_groups.ts | 27 +- .../apm/server/lib/errors/queries.test.ts | 67 ++ .../convert_ui_filters/get_ui_filters_es.ts | 29 +- .../__snapshots__/queries.test.ts.snap | 448 ++++++++++++ .../metrics/fetch_and_transform_metrics.ts | 26 +- .../apm/server/lib/metrics/queries.test.ts | 57 ++ .../__snapshots__/queries.test.ts.snap | 235 +++++++ .../get_services/get_services_items.ts | 29 +- .../apm/server/lib/services/queries.test.ts | 57 ++ .../__snapshots__/queries.test.ts.snap | 168 +++++ .../agent_configuration/queries.test.ts | 88 +++ .../traces/__snapshots__/queries.test.ts.snap | 63 ++ .../apm/server/lib/traces/queries.test.ts | 25 + .../__snapshots__/fetcher.test.ts.snap | 5 +- .../__snapshots__/queries.test.ts.snap | 192 +++++ .../server/lib/transaction_groups/fetcher.ts | 48 +- .../lib/transaction_groups/queries.test.ts | 47 ++ .../__snapshots__/queries.test.ts.snap | 657 ++++++++++++++++++ .../server/lib/transactions/queries.test.ts | 107 +++ .../__snapshots__/queries.test.ts.snap | 96 +++ .../__snapshots__/queries.test.ts.snap | 101 +++ .../lib/ui_filters/local_ui_filters/config.ts | 77 ++ .../get_filter_aggregations.ts | 72 ++ .../lib/ui_filters/local_ui_filters/index.ts | 77 ++ .../local_ui_filters/queries.test.ts | 42 ++ .../apm/server/lib/ui_filters/queries.test.ts | 31 + .../plugins/apm/server/routes/ui_filters.ts | 174 ++++- .../plugins/apm/typings/elasticsearch.ts | 31 +- .../legacy/plugins/apm/typings/ui-filters.ts | 6 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 67 files changed, 4679 insertions(+), 325 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/common/projections/errors.ts create mode 100644 x-pack/legacy/plugins/apm/common/projections/metrics.ts create mode 100644 x-pack/legacy/plugins/apm/common/projections/services.ts create mode 100644 x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts create mode 100644 x-pack/legacy/plugins/apm/common/projections/transactions.ts create mode 100644 x-pack/legacy/plugins/apm/common/projections/typings.ts create mode 100644 x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.test.ts create mode 100644 x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/__snapshots__/TransactionOverview.test.tsx.snap create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx create mode 100644 x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/pickKeys.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/errors/distribution/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/traces/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/transaction_groups/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/transactions/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/ui_filters/queries.test.ts diff --git a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index a7c61d1c79da4..3ba2bd2767773 100644 --- a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Error CONTAINER_ID 1`] = `undefined`; + exports[`Error ERROR_CULPRIT 1`] = `"handleOopsie"`; exports[`Error ERROR_EXC_HANDLED 1`] = `undefined`; @@ -12,6 +14,8 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Error ERROR_PAGE_URL 1`] = `undefined`; +exports[`Error HOST_NAME 1`] = `"my hostname"`; + exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Error METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -42,6 +46,8 @@ exports[`Error OBSERVER_VERSION_MAJOR 1`] = `undefined`; exports[`Error PARENT_ID 1`] = `"parentId"`; +exports[`Error POD_NAME 1`] = `undefined`; + exports[`Error PROCESSOR_EVENT 1`] = `"error"`; exports[`Error SERVICE_AGENT_NAME 1`] = `"java"`; @@ -86,6 +92,8 @@ exports[`Error URL_FULL 1`] = `undefined`; exports[`Error USER_ID 1`] = `undefined`; +exports[`Span CONTAINER_ID 1`] = `undefined`; + exports[`Span ERROR_CULPRIT 1`] = `undefined`; exports[`Span ERROR_EXC_HANDLED 1`] = `undefined`; @@ -98,6 +106,8 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Span ERROR_PAGE_URL 1`] = `undefined`; +exports[`Span HOST_NAME 1`] = `undefined`; + exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Span METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -128,6 +138,8 @@ exports[`Span OBSERVER_VERSION_MAJOR 1`] = `undefined`; exports[`Span PARENT_ID 1`] = `"parentId"`; +exports[`Span POD_NAME 1`] = `undefined`; + exports[`Span PROCESSOR_EVENT 1`] = `"span"`; exports[`Span SERVICE_AGENT_NAME 1`] = `"java"`; @@ -172,6 +184,8 @@ exports[`Span URL_FULL 1`] = `undefined`; exports[`Span USER_ID 1`] = `undefined`; +exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`; + exports[`Transaction ERROR_CULPRIT 1`] = `undefined`; exports[`Transaction ERROR_EXC_HANDLED 1`] = `undefined`; @@ -184,6 +198,8 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`; +exports[`Transaction HOST_NAME 1`] = `"my hostname"`; + exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; exports[`Transaction METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -214,6 +230,8 @@ exports[`Transaction OBSERVER_VERSION_MAJOR 1`] = `undefined`; exports[`Transaction PARENT_ID 1`] = `"parentId"`; +exports[`Transaction POD_NAME 1`] = `undefined`; + exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`; exports[`Transaction SERVICE_AGENT_NAME 1`] = `"java"`; diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts index a5d057817893c..365a9865b6e47 100644 --- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts @@ -57,3 +57,7 @@ export const METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED = 'jvm.memory.non_heap.committed'; export const METRIC_JAVA_NON_HEAP_MEMORY_USED = 'jvm.memory.non_heap.used'; export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count'; + +export const HOST_NAME = 'host.hostname'; +export const CONTAINER_ID = 'container.id'; +export const POD_NAME = 'kubernetes.pod.name'; diff --git a/x-pack/legacy/plugins/apm/common/projections/errors.ts b/x-pack/legacy/plugins/apm/common/projections/errors.ts new file mode 100644 index 0000000000000..c3094f5cbb0b6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/errors.ts @@ -0,0 +1,46 @@ +/* + * 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 { Setup } from '../../server/lib/helpers/setup_request'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + ERROR_GROUP_ID +} from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getErrorGroupsProjection({ + setup, + serviceName +}: { + setup: Setup; + serviceName: string; +}) { + const { start, end, uiFiltersES, config } = setup; + + return { + index: config.get('apm_oss.errorIndices'), + body: { + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'error' } }, + { range: rangeFilter(start, end) }, + ...uiFiltersES + ] + } + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID + } + } + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/metrics.ts b/x-pack/legacy/plugins/apm/common/projections/metrics.ts new file mode 100644 index 0000000000000..51e74e19afb1f --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/metrics.ts @@ -0,0 +1,37 @@ +/* + * 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 { Setup } from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getMetricsProjection({ + setup, + serviceName +}: { + setup: Setup; + serviceName: string; +}) { + const { start, end, uiFiltersES, config } = setup; + + return { + index: config.get('apm_oss.metricsIndices'), + body: { + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { + range: rangeFilter(start, end) + }, + ...uiFiltersES + ] + } + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/services.ts b/x-pack/legacy/plugins/apm/common/projections/services.ts new file mode 100644 index 0000000000000..ab72211f92aa7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/services.ts @@ -0,0 +1,42 @@ +/* + * 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 { Setup } from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getServicesProjection({ setup }: { setup: Setup }) { + const { start, end, uiFiltersES, config } = setup; + + return { + index: [ + config.get('apm_oss.metricsIndices'), + config.get('apm_oss.errorIndices'), + config.get('apm_oss.transactionIndices') + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } + }, + { range: rangeFilter(start, end) }, + ...uiFiltersES + ] + } + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME + } + } + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts b/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts new file mode 100644 index 0000000000000..6f7be349b0cba --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/transaction_groups.ts @@ -0,0 +1,46 @@ +/* + * 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 { omit } from 'lodash'; +import { Setup } from '../../server/lib/helpers/setup_request'; +import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; +import { Options } from '../../server/lib/transaction_groups/fetcher'; +import { getTransactionsProjection } from './transactions'; +import { mergeProjection } from './util/merge_projection'; + +export function getTransactionGroupsProjection({ + setup, + options +}: { + setup: Setup; + options: Options; +}) { + const transactionsProjection = getTransactionsProjection({ + setup, + ...(omit(options, 'type') as Omit) + }); + + const bool = + options.type === 'top_traces' + ? { + must_not: [{ exists: { field: PARENT_ID } }] + } + : {}; + + return mergeProjection(transactionsProjection, { + body: { + query: { + bool + }, + aggs: { + transactions: { + terms: { + field: TRANSACTION_NAME + } + } + } + } + }); +} diff --git a/x-pack/legacy/plugins/apm/common/projections/transactions.ts b/x-pack/legacy/plugins/apm/common/projections/transactions.ts new file mode 100644 index 0000000000000..63abb0572df87 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/transactions.ts @@ -0,0 +1,58 @@ +/* + * 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 { Setup } from '../../server/lib/helpers/setup_request'; +import { + SERVICE_NAME, + TRANSACTION_TYPE, + PROCESSOR_EVENT, + TRANSACTION_NAME +} from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../../server/lib/helpers/range_filter'; + +export function getTransactionsProjection({ + setup, + serviceName, + transactionName, + transactionType +}: { + setup: Setup; + serviceName?: string; + transactionName?: string; + transactionType?: string; +}) { + const { start, end, uiFiltersES, config } = setup; + + const transactionNameFilter = transactionName + ? [{ term: { [TRANSACTION_NAME]: transactionName } }] + : []; + const transactionTypeFilter = transactionType + ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] + : []; + const serviceNameFilter = serviceName + ? [{ term: { [SERVICE_NAME]: serviceName } }] + : []; + + const bool = { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + ...transactionNameFilter, + ...transactionTypeFilter, + ...serviceNameFilter, + ...uiFiltersES + ] + }; + + return { + index: config.get('apm_oss.transactionIndices'), + body: { + query: { + bool + } + } + }; +} diff --git a/x-pack/legacy/plugins/apm/common/projections/typings.ts b/x-pack/legacy/plugins/apm/common/projections/typings.ts new file mode 100644 index 0000000000000..b49dba5e0a6fd --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/typings.ts @@ -0,0 +1,28 @@ +/* + * 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 { SearchParams } from 'elasticsearch'; + +export type Projection = Omit & { + body: { + query: any; + } & { + aggs?: { + [key: string]: { + terms: any; + }; + }; + }; +}; + +export enum PROJECTION { + SERVICES = 'services', + TRANSACTION_GROUPS = 'transactionGroups', + TRACES = 'traces', + TRANSACTIONS = 'transactions', + METRICS = 'metrics', + ERROR_GROUPS = 'errorGroups' +} diff --git a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.test.ts b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.test.ts new file mode 100644 index 0000000000000..ae1b7c552ab4e --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { mergeProjection } from './index'; + +describe('mergeProjection', () => { + it('overrides arrays', () => { + expect( + mergeProjection( + { body: { query: { bool: { must: [{ terms: ['a'] }] } } } }, + { body: { query: { bool: { must: [{ term: 'b' }] } } } } + ) + ).toEqual({ + body: { + query: { + bool: { + must: [ + { + term: 'b' + } + ] + } + } + } + }); + }); + + it('merges plain objects', () => { + expect( + mergeProjection( + { body: { query: {}, aggs: { foo: { terms: { field: 'bar' } } } } }, + { + body: { + aggs: { foo: { aggs: { bar: { terms: { field: 'baz' } } } } } + } + } + ) + ).toEqual({ + body: { + query: {}, + aggs: { + foo: { + terms: { + field: 'bar' + }, + aggs: { + bar: { + terms: { + field: 'baz' + } + } + } + } + } + } + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts new file mode 100644 index 0000000000000..5b6b5b0b7f058 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts @@ -0,0 +1,32 @@ +/* + * 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 { merge, isPlainObject } from 'lodash'; +import { Projection } from '../../typings'; + +type PlainObject = Record; + +type DeepMerge = U extends PlainObject + ? (T extends PlainObject + ? (Omit & + { + [key in keyof U]: T extends { [k in key]: any } + ? DeepMerge + : U[key]; + }) + : U) + : U; + +export function mergeProjection( + target: T, + source: U +): DeepMerge { + return merge({}, target, source, (a, b) => { + if (isPlainObject(a) && isPlainObject(b)) { + return undefined; + } + return b; + }) as DeepMerge; +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 4d5a87faa46a6..57b7ea200ece7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -12,7 +12,7 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useFetcher } from '../../../hooks/useFetcher'; import { loadErrorDistribution, @@ -22,12 +22,13 @@ import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; const ErrorGroupOverview: React.SFC = () => { - const { - urlParams: { serviceName, start, end, sortField, sortDirection }, - uiFilters - } = useUrlParams(); + const { urlParams, uiFilters } = useUrlParams(); + + const { serviceName, start, end, sortField, sortDirection } = urlParams; const { data: errorDistributionData } = useFetcher(() => { if (serviceName && start && end) { @@ -53,42 +54,62 @@ const ErrorGroupOverview: React.SFC = () => { } }, [serviceName, start, end, sortField, sortDirection, uiFilters]); - useTrackPageview({ app: 'apm', path: 'error_group_overview' }); + useTrackPageview({ + app: 'apm', + path: 'error_group_overview' + }); useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionResult', 'host', 'containerId', 'podName'], + params: { + serviceName + }, + projection: PROJECTION.ERROR_GROUPS + }; + + return config; + }, [serviceName]); + if (!errorDistributionData || !errorGroupListData) { return null; } return ( - - - - - - - - + + + + + + + + + + + + - - - - -

Errors

-
- -
-
+ + +

Errors

+
+ + + +
+ + ); }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 4a5f4c53d6e43..a44d1b8313325 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -32,7 +32,7 @@ export function ServiceDetailTabs({ urlParams }: Props) { defaultMessage: 'Transactions' }), path: `/services/${serviceName}/transactions`, - render: () => , + render: () => , name: 'transactions' }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 5e76140ce21e8..871e48c290503 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -4,12 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import { + EuiFlexGrid, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiFlexGroup +} from '@elastic/eui'; +import React, { useMemo } from 'react'; import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from './MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { agentName: string; @@ -17,22 +25,43 @@ interface ServiceMetricsProps { export function ServiceMetrics({ agentName }: ServiceMetricsProps) { const { urlParams } = useUrlParams(); + const { serviceName } = urlParams; const { data } = useServiceMetricCharts(urlParams, agentName); const { start, end } = urlParams; + + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( + () => ({ + filterNames: ['host', 'containerId', 'podName'], + params: { + serviceName + }, + projection: PROJECTION.METRICS, + showCount: false + }), + [serviceName] + ); + return ( - - - - {data.charts.map(chart => ( - - - - - - ))} - - - - + + + + + + + + {data.charts.map(chart => ( + + + + + + ))} + + + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index 013d803e6411d..58b9ae2d868d0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -13,6 +13,8 @@ import { ServiceOverview } from '..'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; import * as coreHooks from '../../../../hooks/useCore'; import { InternalCoreStart } from 'src/core/public'; +import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; jest.mock('ui/kfetch'); @@ -38,6 +40,13 @@ describe('Service Overview -> View', () => { } }); spyOn(coreHooks, 'useCore').and.returnValue(coreMock); + + jest.spyOn(useLocalUIFilters, 'useLocalUIFilters').mockReturnValue({ + filters: [], + setFilterValue: () => null, + clearValues: () => null, + status: FETCH_STATUS.SUCCESS + }); }); afterEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx index 78b6447a4ff19..dd0937da47719 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { toastNotifications } from 'ui/notify'; import url from 'url'; import { useFetcher } from '../../../hooks/useFetcher'; @@ -17,6 +17,8 @@ import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; import { useCore } from '../../../hooks/useCore'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; const initalData = { items: [], @@ -75,17 +77,34 @@ export function ServiceOverview() { useTrackPageview({ app: 'apm', path: 'services_overview' }); useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( + () => ({ + filterNames: ['host', 'agentName'], + projection: PROJECTION.SERVICES + }), + [] + ); + return ( - - + + + + + + + } /> - } - /> - + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx index 2871efca018a1..74819600d44a9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; -import React from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { loadTraceList } from '../../../services/rest/apm/traces'; import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; export function TraceOverview() { const { urlParams, uiFilters } = useUrlParams(); @@ -24,9 +26,25 @@ export function TraceOverview() { useTrackPageview({ app: 'apm', path: 'traces_overview' }); useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 }); + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionResult', 'host', 'containerId', 'podName'], + projection: PROJECTION.TRACES + }; + + return config; + }, []); + return ( - - - + + + + + + + + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx index f242690beab03..03321397b2da2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -4,9 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiSpacer, EuiTitle, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiPanel, + EuiSpacer, + EuiTitle, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem +} from '@elastic/eui'; import _ from 'lodash'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; import { useWaterfall } from '../../../hooks/useWaterfall'; @@ -20,6 +27,8 @@ import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../infra/public'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; export function TransactionDetails() { const location = useLocation(); @@ -34,11 +43,24 @@ export function TransactionDetails() { const { data: waterfall, exceedsMax } = useWaterfall(urlParams); const transaction = waterfall.getTransactionById(urlParams.transactionId); - const { transactionName } = urlParams; + const { transactionName, transactionType, serviceName } = urlParams; useTrackPageview({ app: 'apm', path: 'transaction_details' }); useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 }); + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionResult'], + projection: PROJECTION.TRANSACTIONS, + params: { + transactionName, + transactionType, + serviceName + } + }; + return config; + }, [transactionName, transactionType, serviceName]); + return (
@@ -47,43 +69,50 @@ export function TransactionDetails() { - - + + + + + + + - + - - + + - + - - - + + + - + - {transaction && ( - - )} + {transaction && ( + + )} + +
); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx index 7b1770dd4745b..9bc8101c78983 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx @@ -5,12 +5,26 @@ */ import React from 'react'; -import { queryByLabelText, render } from 'react-testing-library'; -import { TransactionOverview } from '..'; -import * as useLocationHook from '../../../../hooks/useLocation'; +import { + queryByLabelText, + render, + queryBySelectText, + getByText, + getByDisplayValue, + queryByDisplayValue, + fireEvent +} from 'react-testing-library'; +import { omit } from 'lodash'; import { history } from '../../../../utils/history'; +import { TransactionOverview } from '..'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import * as useServiceTransactionTypesHook from '../../../../hooks/useServiceTransactionTypes'; +import { fromQuery } from '../../../shared/Links/url_helpers'; +import { Router } from 'react-router-dom'; +import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; + +jest.spyOn(history, 'push'); +jest.spyOn(history, 'replace'); jest.mock('ui/kfetch'); @@ -31,31 +45,34 @@ function setup({ urlParams: IUrlParams; serviceTransactionTypes: string[]; }) { - jest.spyOn(history, 'replace'); - jest - .spyOn(useLocationHook, 'useLocation') - .mockReturnValue({ pathname: '' } as any); + const defaultLocation = { + pathname: '/services/foo/transactions', + search: fromQuery(omit(urlParams, 'serviceName')) + } as any; + + history.replace({ + ...defaultLocation + }); jest .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') .mockReturnValue(serviceTransactionTypes); - const { container } = render(); - return { container }; + return render( + + + + + + ); } describe('TransactionOverview', () => { - describe('when no transaction type is given', () => { - it('should render null', () => { - const { container } = setup({ - serviceTransactionTypes: ['firstType', 'secondType'], - urlParams: { - serviceName: 'MyServiceName' - } - }); - expect(container).toMatchInlineSnapshot(`
`); - }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('when no transaction type is given', () => { it('should redirect to first type', () => { setup({ serviceTransactionTypes: ['firstType', 'secondType'], @@ -71,7 +88,7 @@ describe('TransactionOverview', () => { }); }); - const FILTER_BY_TYPE_LABEL = 'Filter by type'; + const FILTER_BY_TYPE_LABEL = 'Transaction type'; describe('when transactionType is selected and multiple transaction types are given', () => { it('should render dropdown with transaction types', () => { @@ -83,9 +100,33 @@ describe('TransactionOverview', () => { } }); - expect( - queryByLabelText(container, FILTER_BY_TYPE_LABEL) - ).toMatchSnapshot(); + // secondType is selected in the dropdown + expect(queryBySelectText(container, 'secondType')).not.toBeNull(); + expect(queryBySelectText(container, 'firstType')).toBeNull(); + + expect(getByText(container, 'firstType')).not.toBeNull(); + }); + + it('should update the URL when a transaction type is selected', () => { + const { container } = setup({ + serviceTransactionTypes: ['firstType', 'secondType'], + urlParams: { + transactionType: 'secondType', + serviceName: 'MyServiceName' + } + }); + + expect(queryByDisplayValue(container, 'firstType')).toBeNull(); + + fireEvent.change(getByDisplayValue(container, 'secondType'), { + target: { value: 'firstType' } + }); + + expect(history.push).toHaveBeenCalled(); + + getByDisplayValue(container, 'firstType'); + + expect(queryByDisplayValue(container, 'firstType')).not.toBeNull(); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/__snapshots__/TransactionOverview.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/__snapshots__/TransactionOverview.test.tsx.snap deleted file mode 100644 index 034e39de54da7..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/__snapshots__/TransactionOverview.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TransactionOverview when transactionType is selected and multiple transaction types are given should render dropdown with transaction types 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx index e2244e7ea1087..52450bc3876f3 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -5,16 +5,16 @@ */ import { - EuiFormRow, EuiPanel, - EuiSelect, EuiSpacer, - EuiTitle + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import { first } from 'lodash'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; @@ -29,11 +29,11 @@ import { useLocation } from '../../../hooks/useLocation'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../infra/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; - -interface Props { - urlParams: IUrlParams; -} +import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; function getRedirectLocation({ urlParams, @@ -58,8 +58,9 @@ function getRedirectLocation({ } } -export function TransactionOverview({ urlParams }: Props) { +export function TransactionOverview() { const location = useLocation(); + const { urlParams } = useUrlParams(); const { serviceName, transactionType } = urlParams; // TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context? @@ -90,6 +91,20 @@ export function TransactionOverview({ urlParams }: Props) { } }, [serviceName, transactionType]); + const localFiltersConfig: React.ComponentProps< + typeof LocalUIFilters + > = useMemo( + () => ({ + filterNames: ['transactionResult', 'host', 'containerId', 'podName'], + params: { + serviceName, + transactionType + }, + projection: PROJECTION.TRANSACTION_GROUPS + }), + [serviceName, transactionType] + ); + // TODO: improve urlParams typings. // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed if (!serviceName || !transactionType) { @@ -97,63 +112,41 @@ export function TransactionOverview({ urlParams }: Props) { } return ( - - {/* TODO: This should be replaced by local filters */} - {serviceTransactionTypes.length > 1 ? ( - - ({ - text: `${type}`, - value: type - }))} - value={transactionType} - onChange={event => { - history.push({ - ...location, - pathname: `/services/${urlParams.serviceName}/transactions`, - search: fromQuery({ - ...toQuery(location.search), - transactionType: event.target.value - }) - }); - }} + + + + + + + + + + + + + + + - - ) : null} - - - + - - - - - - - -

Transactions

-
- - -
-
+ + +

Transactions

+
+ + +
+ + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx index f1eac0a08142e..78fa47e87a598 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/HistoryTabs/index.tsx @@ -8,6 +8,7 @@ import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import React from 'react'; import { matchPath, Route, RouteComponentProps } from 'react-router-dom'; import { omit } from 'lodash'; +import { localUIFilterNames } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { useLocation } from '../../../hooks/useLocation'; import { history } from '../../../utils/history'; import { toQuery, fromQuery } from '../Links/url_helpers'; @@ -39,17 +40,18 @@ export function HistoryTabs({ tabs }: HistoryTabsProps) { {tabs.map((tab, i) => ( { - const searchWithoutTableParameters = omit( + const persistedQueryParameters = omit( toQuery(location.search), 'sortField', 'sortDirection', 'page', - 'pageSize' + 'pageSize', + ...localUIFilterNames ); history.push({ ...location, pathname: tab.path, - search: fromQuery(searchWithoutTableParameters) + search: fromQuery(persistedQueryParameters) }); }} isSelected={isTabSelected(tab, location.pathname)} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts index 0575c0837668c..763ee93df6415 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -5,6 +5,7 @@ */ import qs from 'querystring'; +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { StringMap } from '../../../../typings/common'; export function toQuery(search?: string): APMQueryParamsRaw { @@ -19,7 +20,7 @@ export function fromQuery(query: StringMap) { }); } -export interface APMQueryParams { +export type APMQueryParams = { transactionId?: string; transactionName?: string; transactionType?: string; @@ -38,7 +39,7 @@ export interface APMQueryParams { rangeTo?: string; refreshPaused?: string | boolean; refreshInterval?: string | number; -} +} & { [key in LocalUIFilterName]?: string }; // forces every value of T[K] to be type: string type StringifyAll = { [K in keyof T]: string }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx new file mode 100644 index 0000000000000..a1140dcbbb3fc --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -0,0 +1,43 @@ +/* + * 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 React from 'react'; +import { EuiFlexGrid, EuiFlexItem, EuiBadge, EuiIcon } from '@elastic/eui'; +import styled from 'styled-components'; +import { unit, px, truncate } from '../../../../style/variables'; + +const BadgeText = styled.div` + display: inline-block; + ${truncate(px(unit * 8))}; + vertical-align: middle; +`; + +interface Props { + value: string[]; + onRemove: (val: string) => void; +} + +const FilterBadgeList = ({ onRemove, value }: Props) => ( + + {value.map(val => ( + + + + ))} + +); + +export { FilterBadgeList }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx new file mode 100644 index 0000000000000..a257e52ab01cc --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; +import { EuiButtonEmpty, EuiTitle } from '@elastic/eui'; +import styled from 'styled-components'; + +const Button = styled(EuiButtonEmpty).attrs({ + contentProps: { + className: 'alignLeft' + }, + color: 'text' +})` + width: 100%; + + .alignLeft { + justify-content: flex-start; + padding-left: 0; + } +`; + +type Props = React.ComponentProps; + +export const FilterTitleButton = (props: Props) => { + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx new file mode 100644 index 0000000000000..9f13ee3fbe59d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx @@ -0,0 +1,173 @@ +/* + * 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 React, { useState, useMemo, useEffect } from 'react'; +import { + EuiTitle, + EuiPopover, + EuiSelectable, + EuiSpacer, + EuiHorizontalRule, + EuiText, + EuiButton, + EuiFlexItem, + EuiFlexGroup +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import styled from 'styled-components'; +import { FilterBadgeList } from './FilterBadgeList'; +import { unit, px } from '../../../../style/variables'; +import { FilterTitleButton } from './FilterTitleButton'; + +const Popover = styled(EuiPopover).attrs({ + anchorClassName: 'anchor' +})` + .anchor { + display: block; + } +`; + +const SelectContainer = styled.div` + width: ${px(unit * 16)}; +`; + +const Counter = styled.div` + border-radius: ${theme.euiBorderRadius}; + background: ${theme.euiColorLightShade}; + padding: 0 ${theme.paddingSizes.xs}; +`; + +const ApplyButton = styled(EuiButton)` + align-self: flex-end; +`; + +interface Props { + name: string; + title: string; + options: Array<{ + name: string; + count: number; + }>; + onChange: (value: string[]) => void; + value: string[]; + showCount: boolean; +} + +type Option = EuiSelectable['props']['options'][0]; + +const Filter = ({ + name, + title, + options, + onChange, + value, + showCount +}: Props) => { + const [showPopover, setShowPopover] = useState(false); + + const toggleShowPopover = () => setShowPopover(show => !show); + + const button = ( + {title} + ); + + const items: Option[] = useMemo( + () => + options.map(option => ({ + label: option.name, + append: showCount ? ( + + {option.count} + + ) : null, + checked: value.includes(option.name) ? 'on' : undefined + })), + [value, options, showCount] + ); + + const [visibleOptions, setVisibleOptions] = useState(items); + + useEffect(() => { + setVisibleOptions(items); + }, [items]); + + return ( + <> + + { + setVisibleOptions(selectedOptions); + }} + options={visibleOptions} + searchable={true} + > + {(list, search) => ( + + + + +

+ {i18n.translate('xpack.apm.applyFilter', { + defaultMessage: 'Apply {title} filter', + values: { title } + })} +

+
+ + + {search} + + {list} + +
+ + { + const newValue = visibleOptions + .filter(option => option.checked === 'on') + .map(option => option.label); + + setShowPopover(false); + onChange(newValue); + }} + size="s" + > + {i18n.translate('xpack.apm.applyOptions', { + defaultMessage: 'Apply options' + })} + + +
+
+ )} +
+
+ {value.length ? ( + <> + { + onChange(value.filter(v => val !== v)); + }} + value={value} + /> + + + ) : null} + + ); +}; + +export { Filter }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx new file mode 100644 index 0000000000000..f043da824d375 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx @@ -0,0 +1,64 @@ +/* + * 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 React from 'react'; +import { + EuiTitle, + EuiHorizontalRule, + EuiSpacer, + EuiSelect +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../Links/url_helpers'; + +interface Props { + transactionTypes: string[]; +} + +const TransactionTypeFilter = ({ transactionTypes }: Props) => { + const { + urlParams: { transactionType } + } = useUrlParams(); + + const options = transactionTypes.map(type => ({ + text: type, + value: type + })); + + return ( + <> + +

+ {i18n.translate('xpack.apm.localFilters.titles.transactionType', { + defaultMessage: 'Transaction type' + })} +

+
+ + + + { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + transactionType: event.target.value + }) + }; + history.push(newLocation); + }} + /> + + ); +}; + +export { TransactionTypeFilter }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx new file mode 100644 index 0000000000000..ef05c21224c69 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -0,0 +1,88 @@ +/* + * 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 React from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiHorizontalRule, + EuiButtonEmpty +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; +import { Filter } from './Filter'; +import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; + +interface Props { + projection: PROJECTION; + filterNames: LocalUIFilterName[]; + params?: Record; + showCount?: boolean; + children?: React.ReactNode; +} + +const ButtonWrapper = styled.div` + display: inline-block; +`; + +const LocalUIFilters = ({ + projection, + params, + filterNames, + children, + showCount = true +}: Props) => { + const { filters, setFilterValue, clearValues } = useLocalUIFilters({ + filterNames, + projection, + params + }); + + return ( + <> + +

+ {i18n.translate('xpack.apm.localFiltersTitle', { + defaultMessage: 'Filters' + })} +

+
+ + {children} + {filters.map(filter => { + return ( + + { + setFilterValue(filter.name, value); + }} + showCount={showCount} + /> + + + ); + })} + + + + {i18n.translate('xpack.apm.clearFilters', { + defaultMessage: 'Clear filters' + })} + + + + ); +}; + +export { LocalUIFilters }; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx index 8cf326d6cfd6a..a26c2797135d3 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx @@ -12,19 +12,34 @@ import React, { useState } from 'react'; import { withRouter } from 'react-router-dom'; -import { uniqueId } from 'lodash'; +import { uniqueId, mapValues } from 'lodash'; import { IUrlParams } from './types'; import { getParsedDate } from './helpers'; import { resolveUrlParams } from './resolveUrlParams'; import { UIFilters } from '../../../typings/ui-filters'; +import { + localUIFilterNames, + LocalUIFilterName +} from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../utils/pickKeys'; interface TimeRange { rangeFrom: string; rangeTo: string; } -function useUiFilters({ kuery, environment }: IUrlParams): UIFilters { - return useMemo(() => ({ kuery, environment }), [kuery, environment]); +function useUiFilters( + params: Pick +): UIFilters { + return useMemo(() => { + const { kuery, environment, ...localUIFilters } = params; + const mappedLocalFilters = mapValues( + pickKeys(localUIFilters, ...localUIFilterNames), + val => (val ? val.split(',') : []) + ) as Partial>; + + return { kuery, environment, ...mappedLocalFilters }; + }, [params]); } const defaultRefresh = (time: TimeRange) => {}; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index 234bbc55a1069..06abf9c0945b7 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -17,6 +17,8 @@ import { } from './helpers'; import { toQuery } from '../../components/shared/Links/url_helpers'; import { TIMEPICKER_DEFAULTS } from './constants'; +import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../utils/pickKeys'; type TimeUrlParams = Pick< IUrlParams, @@ -28,6 +30,8 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { location.pathname ); + const query = toQuery(location.search); + const { traceId, transactionId, @@ -47,7 +51,9 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { rangeFrom = TIMEPICKER_DEFAULTS.rangeFrom, rangeTo = TIMEPICKER_DEFAULTS.rangeTo, environment - } = toQuery(location.search); + } = query; + + const localUIFilters = pickKeys(query, ...localUIFilterNames); return removeUndefinedProps({ // date params @@ -79,6 +85,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { errorGroupId, // ui filters - environment + environment, + ...localUIFilters }); } diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts index bb4af42b7a73a..979fd3e8d60bd 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IUrlParams { +import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; + +export type IUrlParams = { detailTab?: string; end?: string; errorGroupId?: string; @@ -26,4 +28,4 @@ export interface IUrlParams { waterfallItemId?: string; page?: number; pageSize?: number; -} +} & Partial>; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts new file mode 100644 index 0000000000000..4158c37c2a823 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -0,0 +1,82 @@ +/* + * 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 { omit } from 'lodash'; +import { useFetcher } from './useFetcher'; +import { callApi } from '../services/rest/callApi'; +import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; +import { useUrlParams } from './useUrlParams'; +import { LocalUIFilterName } from '../../server/lib/ui_filters/local_ui_filters/config'; +import { history } from '../utils/history'; +import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; +import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; +import { PROJECTION } from '../../common/projections/typings'; +import { pickKeys } from '../utils/pickKeys'; + +const initialData = [] as LocalUIFiltersAPIResponse; + +export function useLocalUIFilters({ + projection, + filterNames, + params +}: { + projection: PROJECTION; + filterNames: LocalUIFilterName[]; + params?: Record; +}) { + const { uiFilters, urlParams } = useUrlParams(); + + const values = pickKeys(uiFilters, ...filterNames); + + const setFilterValue = (name: LocalUIFilterName, value: string[]) => { + const search = omit(toQuery(history.location.search), name); + + history.push({ + ...history.location, + search: fromQuery( + removeUndefinedProps({ + ...search, + [name]: value.length ? value.join(',') : undefined + }) + ) + }); + }; + + const clearValues = () => { + const search = omit(toQuery(history.location.search), filterNames); + history.push({ + ...history.location, + search: fromQuery(search) + }); + }; + + const { data = initialData, status } = useFetcher(async () => { + const foo = await callApi({ + method: 'GET', + pathname: `/api/apm/ui_filters/local_filters/${projection}`, + query: { + uiFilters: JSON.stringify(uiFilters), + start: urlParams.start, + end: urlParams.end, + filterNames: JSON.stringify(filterNames), + ...params + } + }); + return foo; + }, [uiFilters, urlParams, params, filterNames, projection]); + + const filters = data.map(filter => ({ + ...filter, + value: values[filter.name] || [] + })); + + return { + filters, + status, + setFilterValue, + clearValues + }; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts b/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts new file mode 100644 index 0000000000000..f3ab62d33cf55 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts @@ -0,0 +1,10 @@ +/* + * 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 { pick } from 'lodash'; + +export function pickKeys(obj: T, ...keys: K[]) { + return pick(obj, keys) as Pick; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index bfd6db2e92b55..2a772f4300df4 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -15,7 +15,9 @@ import { Moment } from 'moment-timezone'; import React from 'react'; import { render, waitForElement } from 'react-testing-library'; import { MemoryRouter } from 'react-router-dom'; +import { ESFilter } from 'elasticsearch'; import { LocationProvider } from '../context/LocationContext'; +import { PromiseReturnType } from '../../typings/common'; export function toJson(wrapper: ReactWrapper) { return enzymeToJson(wrapper, { @@ -90,3 +92,53 @@ export function expectTextsInDocument(output: any, texts: string[]) { expect(output.getByText(text)).toBeInTheDocument(); }); } + +interface MockSetup { + start: number; + end: number; + client: any; + config: { + get: any; + has: any; + }; + uiFiltersES: ESFilter[]; +} + +export async function inspectSearchParams( + fn: (mockSetup: MockSetup) => Promise +) { + const clientSpy = jest.fn().mockReturnValueOnce({ + hits: { + total: 0 + } + }); + + const mockSetup = { + start: 1528113600000, + end: 1528977600000, + client: { + search: clientSpy + } as any, + config: { + get: () => 'myIndex' as any, + has: () => true + }, + uiFiltersES: [ + { + term: { 'service.environment': 'prod' } + } + ] + }; + try { + await fn(mockSetup); + } catch { + // we're only extracting the search params + } + + return { + params: clientSpy.mock.calls[0][0], + teardown: () => clientSpy.mockClear() + }; +} + +export type SearchParamsMock = PromiseReturnType; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..76b2d67199bf7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -0,0 +1,248 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`error queries fetches a single error group 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "term": Object { + "error.grouping_key": "groupId", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "size": 1, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + "index": "myIndex", +} +`; + +exports[`error queries fetches multiple error groups 1`] = ` +Object { + "body": Object { + "aggs": Object { + "error_groups": Object { + "aggs": Object { + "sample": Object { + "top_hits": Object { + "_source": Array [ + "error.log.message", + "error.exception.message", + "error.exception.handled", + "error.culprit", + "error.grouping_key", + "@timestamp", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": "desc", + }, + ], + }, + }, + }, + "terms": Object { + "field": "error.grouping_key", + "order": Object { + "_count": "asc", + }, + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = ` +Object { + "body": Object { + "aggs": Object { + "error_groups": Object { + "aggs": Object { + "max_timestamp": Object { + "max": Object { + "field": "@timestamp", + }, + }, + "sample": Object { + "top_hits": Object { + "_source": Array [ + "error.log.message", + "error.exception.message", + "error.exception.handled", + "error.culprit", + "error.grouping_key", + "@timestamp", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": "desc", + }, + ], + }, + }, + }, + "terms": Object { + "field": "error.grouping_key", + "order": Object { + "max_timestamp": "asc", + }, + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`error queries fetches trace errors 1`] = ` +Object { + "body": Object { + "aggs": Object { + "transactions": Object { + "terms": Object { + "field": "transaction.id", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "trace.id": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..e1d7e9843bf69 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`error distribution queries fetches an error distribution 1`] = ` +Object { + "body": Object { + "aggs": Object { + "distribution": Object { + "histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "interval": NaN, + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`error distribution queries fetches an error distribution with a group id 1`] = ` +Object { + "body": Object { + "aggs": Object { + "distribution": Object { + "histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "interval": NaN, + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "error", + }, + }, + Object { + "term": Object { + "service.name": "serviceName", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "error.grouping_key": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/queries.test.ts new file mode 100644 index 0000000000000..fcc456c653303 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/queries.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { getErrorDistribution } from './get_distribution'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../../public/utils/testHelpers'; + +describe('error distribution queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches an error distribution', async () => { + mock = await inspectSearchParams(setup => + getErrorDistribution({ + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches an error distribution with a group id', async () => { + mock = await inspectSearchParams(setup => + getErrorDistribution({ + serviceName: 'serviceName', + groupId: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts index 33a93fa986db3..df37568c770fc 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_groups.ts @@ -10,14 +10,13 @@ import { ERROR_EXC_HANDLED, ERROR_EXC_MESSAGE, ERROR_GROUP_ID, - ERROR_LOG_MESSAGE, - PROCESSOR_EVENT, - SERVICE_NAME + ERROR_LOG_MESSAGE } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; import { APMError } from '../../../typings/es_schemas/ui/APMError'; -import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; +import { getErrorGroupsProjection } from '../../../common/projections/errors'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; export type ErrorGroupListAPIResponse = PromiseReturnType< typeof getErrorGroups @@ -34,29 +33,19 @@ export async function getErrorGroups({ sortDirection: string; setup: Setup; }) { - const { start, end, uiFiltersES, client, config } = setup; + const { client } = setup; // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; - const params = { - index: config.get('apm_oss.errorIndices'), + const projection = getErrorGroupsProjection({ setup, serviceName }); + + const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, - { range: rangeFilter(start, end) }, - ...uiFiltersES - ] - } - }, aggs: { error_groups: { terms: { - field: ERROR_GROUP_ID, size: 500, order: sortByLatestOccurrence ? { @@ -92,7 +81,7 @@ export async function getErrorGroups({ } } } - }; + }); interface SampleError { '@timestamp': APMError['@timestamp']; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts new file mode 100644 index 0000000000000..2b1704d9424e4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { getErrorGroup } from './get_error_group'; +import { getErrorGroups } from './get_error_groups'; +import { getTraceErrorsPerTransaction } from './get_trace_errors_per_transaction'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('error queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches a single error group', async () => { + mock = await inspectSearchParams(setup => + getErrorGroup({ + groupId: 'groupId', + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches multiple error groups', async () => { + mock = await inspectSearchParams(setup => + getErrorGroups({ + sortDirection: 'asc', + sortField: 'foo', + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches multiple error groups when sortField = latestOccurrenceAt', async () => { + mock = await inspectSearchParams(setup => + getErrorGroups({ + sortDirection: 'asc', + sortField: 'latestOccurrenceAt', + serviceName: 'serviceName', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches trace errors', async () => { + mock = await inspectSearchParams(setup => + getTraceErrorsPerTransaction('foo', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts index 8a27799cdfe68..1658cb07e4d2f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts @@ -9,12 +9,33 @@ import { ESFilter } from 'elasticsearch'; import { UIFilters } from '../../../../typings/ui-filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; import { getKueryUiFilterES } from './get_kuery_ui_filter_es'; +import { + localUIFilters, + localUIFilterNames +} from '../../ui_filters/local_ui_filters/config'; export async function getUiFiltersES(server: Server, uiFilters: UIFilters) { - const kuery = await getKueryUiFilterES(server, uiFilters.kuery); - const environment = getEnvironmentUiFilterES(uiFilters.environment); + const { kuery, environment, ...localFilterValues } = uiFilters; + + const mappedFilters = localUIFilterNames + .filter(name => name in localFilterValues) + .map(filterName => { + const field = localUIFilters[filterName]; + const value = localFilterValues[filterName]; + return { + terms: { + [field.fieldName]: value + } + }; + }) as ESFilter[]; // remove undefined items from list - const filters = [kuery, environment].filter(filter => !!filter) as ESFilter[]; - return filters; + const esFilters = [ + await getKueryUiFilterES(server, uiFilters.kuery), + getEnvironmentUiFilterES(uiFilters.environment) + ] + .filter(filter => !!filter) + .concat(mappedFilters) as ESFilter[]; + + return esFilters; } diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..16d03c0b50498 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -0,0 +1,448 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`metrics queries fetches cpu chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "processCPUAverage": Object { + "avg": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "processCPUMax": Object { + "max": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "systemCPUAverage": Object { + "avg": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + "systemCPUMax": Object { + "max": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "processCPUAverage": Object { + "avg": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "processCPUMax": Object { + "max": Object { + "field": "system.process.cpu.total.norm.pct", + }, + }, + "systemCPUAverage": Object { + "avg": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + "systemCPUMax": Object { + "max": Object { + "field": "system.cpu.total.norm.pct", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches heap memory chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "heapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.heap.committed", + }, + }, + "heapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.heap.max", + }, + }, + "heapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.heap.used", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "heapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.heap.committed", + }, + }, + "heapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.heap.max", + }, + }, + "heapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.heap.used", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "agent.name": "java", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches memory chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "memoryUsedAvg": Object { + "avg": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + "memoryUsedMax": Object { + "max": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + "timeseriesData": Object { + "aggs": Object { + "memoryUsedAvg": Object { + "avg": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + "memoryUsedMax": Object { + "max": Object { + "script": Object { + "lang": "expression", + "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + }, + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "exists": Object { + "field": "system.memory.actual.free", + }, + }, + Object { + "exists": Object { + "field": "system.memory.total", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches non heap memory chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "nonHeapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.non_heap.committed", + }, + }, + "nonHeapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.non_heap.max", + }, + }, + "nonHeapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.non_heap.used", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "nonHeapMemoryCommitted": Object { + "avg": Object { + "field": "jvm.memory.non_heap.committed", + }, + }, + "nonHeapMemoryMax": Object { + "avg": Object { + "field": "jvm.memory.non_heap.max", + }, + }, + "nonHeapMemoryUsed": Object { + "avg": Object { + "field": "jvm.memory.non_heap.used", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "agent.name": "java", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`metrics queries fetches thread count chart data 1`] = ` +Object { + "body": Object { + "aggs": Object { + "threadCount": Object { + "avg": Object { + "field": "jvm.thread.count", + }, + }, + "threadCountMax": Object { + "max": Object { + "field": "jvm.thread.count", + }, + }, + "timeseriesData": Object { + "aggs": Object { + "threadCount": Object { + "avg": Object { + "field": "jvm.thread.count", + }, + }, + "threadCountMax": Object { + "max": Object { + "field": "jvm.thread.count", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "metric", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "agent.name": "java", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index eefa1e0ef201a..89b263612359e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - PROCESSOR_EVENT, - SERVICE_NAME -} from '../../../common/elasticsearch_fieldnames'; import { Setup } from '../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../helpers/metrics'; -import { rangeFilter } from '../helpers/range_filter'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; +import { getMetricsProjection } from '../../../common/projections/metrics'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; interface Aggs { [key: string]: { @@ -45,23 +42,16 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, uiFiltersES, client, config } = setup; + const { start, end, client } = setup; - const params = { - index: config.get('apm_oss.metricsIndices'), + const projection = getMetricsProjection({ setup, serviceName }); + + const params = mergeProjection(projection, { body: { size: 0, query: { bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { - range: rangeFilter(start, end) - }, - ...additionalFilters, - ...uiFiltersES - ] + filter: [...projection.body.query.bool.filter, ...additionalFilters] } }, aggs: { @@ -72,7 +62,7 @@ export async function fetchAndTransformMetrics({ ...aggs } } - }; + }); const response = await client.search(params); diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts new file mode 100644 index 0000000000000..8ee835675e50e --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { getCPUChartData } from './by_agent/shared/cpu'; +import { getMemoryChartData } from './by_agent/shared/memory'; +import { getHeapMemoryChart } from './by_agent/java/heap_memory'; +import { getNonHeapMemoryChart } from './by_agent/java/non_heap_memory'; +import { getThreadCountChart } from './by_agent/java/thread_count'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('metrics queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches cpu chart data', async () => { + mock = await inspectSearchParams(setup => getCPUChartData(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches memory chart data', async () => { + mock = await inspectSearchParams(setup => getMemoryChartData(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches heap memory chart data', async () => { + mock = await inspectSearchParams(setup => getHeapMemoryChart(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches non heap memory chart data', async () => { + mock = await inspectSearchParams(setup => + getNonHeapMemoryChart(setup, 'foo') + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches thread count chart data', async () => { + mock = await inspectSearchParams(setup => + getThreadCountChart(setup, 'foo') + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..ddd800dc22bf3 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`services queries fetches the agent status 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "error", + "metric", + "sourcemap", + "transaction", + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + "myIndex", + ], + "terminateAfter": 1, +} +`; + +exports[`services queries fetches the legacy data status 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + ], + }, + }, + Object { + "range": Object { + "observer.version_major": Object { + "lt": 7, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + ], + "terminateAfter": 1, +} +`; + +exports[`services queries fetches the service agent name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "agents": Object { + "terms": Object { + "field": "agent.name", + "size": 1, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "error", + "transaction", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], + "terminate_after": 1, +} +`; + +exports[`services queries fetches the service items 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "aggs": Object { + "agents": Object { + "terms": Object { + "field": "agent.name", + "size": 1, + }, + }, + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "environments": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "events": Object { + "terms": Object { + "field": "processor.event", + "size": 2, + }, + }, + }, + "terms": Object { + "field": "service.name", + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`services queries fetches the service transaction types 1`] = ` +Object { + "body": Object { + "aggs": Object { + "types": Object { + "terms": Object { + "field": "transaction.type", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts index 75410b70e0139..c50506db1faec 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -5,44 +5,29 @@ */ import { idx } from '@kbn/elastic-idx'; +import { mergeProjection } from '../../../../common/projections/util/merge_projection'; import { PROCESSOR_EVENT, SERVICE_AGENT_NAME, SERVICE_ENVIRONMENT, - SERVICE_NAME, TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../../typings/common'; -import { rangeFilter } from '../../helpers/range_filter'; import { Setup } from '../../helpers/setup_request'; +import { getServicesProjection } from '../../../../common/projections/services'; export type ServiceListAPIResponse = PromiseReturnType; export async function getServicesItems(setup: Setup) { - const { start, end, uiFiltersES, client, config } = setup; + const { start, end, client } = setup; - const params = { - index: [ - config.get('apm_oss.metricsIndices'), - config.get('apm_oss.errorIndices'), - config.get('apm_oss.transactionIndices') - ], + const projection = getServicesProjection({ setup }); + + const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } - }, - { range: rangeFilter(start, end) }, - ...uiFiltersES - ] - } - }, aggs: { services: { terms: { - field: SERVICE_NAME, size: 500 }, aggs: { @@ -62,7 +47,7 @@ export async function getServicesItems(setup: Setup) { } } } - }; + }); const resp = await client.search(params); const aggs = resp.aggregations; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/services/queries.test.ts new file mode 100644 index 0000000000000..83b136112de35 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/queries.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { getServiceAgentName } from './get_service_agent_name'; +import { getServiceTransactionTypes } from './get_service_transaction_types'; +import { getServicesItems } from './get_services/get_services_items'; +import { getLegacyDataStatus } from './get_services/get_legacy_data_status'; +import { getAgentStatus } from './get_services/get_agent_status'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('services queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches the service agent name', async () => { + mock = await inspectSearchParams(setup => + getServiceAgentName('foo', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the service transaction types', async () => { + mock = await inspectSearchParams(setup => + getServiceTransactionTypes('foo', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the service items', async () => { + mock = await inspectSearchParams(setup => getServicesItems(setup)); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the legacy data status', async () => { + mock = await inspectSearchParams(setup => getLegacyDataStatus(setup)); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches the agent status', async () => { + mock = await inspectSearchParams(setup => getAgentStatus(setup)); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..4c53563aa41df --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`agent configuration queries fetches all environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`agent configuration queries fetches configurations 1`] = ` +Object { + "index": "myIndex", +} +`; + +exports[`agent configuration queries fetches filtered configurations with an environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "service.environment": "bar", + }, + }, + ], + }, + }, + "size": 1, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries fetches filtered configurations without an environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "bool": Object { + "must_not": Object { + "exists": Object { + "field": "service.environment", + }, + }, + }, + }, + ], + }, + }, + "size": 1, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries fetches service names 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`agent configuration queries fetches unavailable environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts new file mode 100644 index 0000000000000..768651c8acec7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { getAllEnvironments } from './get_environments/get_all_environments'; +import { getUnavailableEnvironments } from './get_environments/get_unavailable_environments'; +import { getServiceNames } from './get_service_names'; +import { listConfigurations } from './list_configurations'; +import { searchConfigurations } from './search'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../../public/utils/testHelpers'; + +describe('agent configuration queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches all environments', async () => { + mock = await inspectSearchParams(setup => + getAllEnvironments({ + serviceName: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches unavailable environments', async () => { + mock = await inspectSearchParams(setup => + getUnavailableEnvironments({ + serviceName: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches service names', async () => { + mock = await inspectSearchParams(setup => + getServiceNames({ + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches configurations', async () => { + mock = await inspectSearchParams(setup => + listConfigurations({ + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches filtered configurations without an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + serviceName: 'foo', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches filtered configurations with an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + serviceName: 'foo', + environment: 'bar', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..396e8540afdd6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`trace queries fetches a trace 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "trace.id": "foo", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "span", + "transaction", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + "should": Object { + "exists": Object { + "field": "parent.id", + }, + }, + }, + }, + "size": "myIndex", + "sort": Array [ + Object { + "_score": Object { + "order": "asc", + }, + }, + Object { + "transaction.duration.us": Object { + "order": "desc", + }, + }, + Object { + "span.duration.us": Object { + "order": "desc", + }, + }, + ], + }, + "index": Array [ + "myIndex", + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/traces/queries.test.ts new file mode 100644 index 0000000000000..871d0fd1c7fb6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/traces/queries.test.ts @@ -0,0 +1,25 @@ +/* + * 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 { getTraceItems } from './get_trace_items'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('trace queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches a trace', async () => { + mock = await inspectSearchParams(setup => getTraceItems('foo', setup)); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index c55e54938aaba..18ce29982b616 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -168,7 +168,7 @@ Array [ }, Object { "term": Object { - "service.environment": "test", + "transaction.type": "request", }, }, Object { @@ -178,11 +178,10 @@ Array [ }, Object { "term": Object { - "transaction.type": "request", + "service.environment": "test", }, }, ], - "must_not": Array [], "should": Array [ Object { "term": Object { diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..e33255b5baa55 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -0,0 +1,192 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transaction group queries fetches top traces 1`] = ` +Object { + "body": Object { + "aggs": Object { + "transactions": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "p95": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + ], + }, + }, + "sample": Object { + "top_hits": Object { + "size": 1, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "sum": Object { + "sum": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "transaction.name", + "order": Object { + "sum": "desc", + }, + "size": "myIndex", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + "must_not": Array [ + Object { + "exists": Object { + "field": "parent.id", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction group queries fetches top transactions 1`] = ` +Object { + "body": Object { + "aggs": Object { + "transactions": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "p95": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + ], + }, + }, + "sample": Object { + "top_hits": Object { + "size": 1, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "sum": Object { + "sum": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "transaction.name", + "order": Object { + "sum": "desc", + }, + "size": "myIndex", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts index 3b32776739c81..a647bd2faff36 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -6,64 +6,48 @@ import { TRANSACTION_DURATION, - TRANSACTION_NAME, - PROCESSOR_EVENT, - PARENT_ID, - TRANSACTION_SAMPLED, - SERVICE_NAME, - TRANSACTION_TYPE + TRANSACTION_SAMPLED } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; import { Setup } from '../helpers/setup_request'; -import { rangeFilter } from '../helpers/range_filter'; -import { BoolQuery } from '../../../typings/elasticsearch'; +import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; interface TopTransactionOptions { type: 'top_transactions'; serviceName: string; transactionType: string; + transactionName?: string; } interface TopTraceOptions { type: 'top_traces'; + transactionName?: string; } export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; export function transactionGroupsFetcher(options: Options, setup: Setup) { - const { client, config, start, end, uiFiltersES } = setup; + const { client, config } = setup; - const bool: BoolQuery = { - must_not: [], - // prefer sampled transactions - should: [{ term: { [TRANSACTION_SAMPLED]: true } }], - filter: [ - { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - ...uiFiltersES - ] - }; + const projection = getTransactionGroupsProjection({ + setup, + options + }); - if (options.type === 'top_traces') { - // A transaction without `parent.id` is considered a "root" transaction, i.e. a trace - bool.must_not.push({ exists: { field: PARENT_ID } }); - } else { - bool.filter.push({ term: { [SERVICE_NAME]: options.serviceName } }); - bool.filter.push({ term: { [TRANSACTION_TYPE]: options.transactionType } }); - } - - const params = { - index: config.get('apm_oss.transactionIndices'), + const params = mergeProjection(projection, { body: { size: 0, query: { - bool + bool: { + // prefer sampled transactions + should: [{ term: { [TRANSACTION_SAMPLED]: true } }] + } }, aggs: { transactions: { terms: { - field: TRANSACTION_NAME, order: { sum: 'desc' }, size: config.get('xpack.apm.ui.transactionGroupBucketSize') }, @@ -86,7 +70,7 @@ export function transactionGroupsFetcher(options: Options, setup: Setup) { } } } - }; + }); return client.search(params); } diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/queries.test.ts new file mode 100644 index 0000000000000..73122d8580134 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { transactionGroupsFetcher } from './fetcher'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('transaction group queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches top transactions', async () => { + mock = await inspectSearchParams(setup => + transactionGroupsFetcher( + { + type: 'top_transactions', + serviceName: 'foo', + transactionType: 'bar' + }, + setup + ) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches top traces', async () => { + mock = await inspectSearchParams(setup => + transactionGroupsFetcher( + { + type: 'top_traces' + }, + setup + ) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..cdc473b567ea8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -0,0 +1,657 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transaction queries fetches a transaction 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.id": "foo", + }, + }, + Object { + "term": Object { + "trace.id": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 1, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches breakdown data for transactions 1`] = ` +Object { + "body": Object { + "aggs": Object { + "by_date": Object { + "aggs": Object { + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches breakdown data for transactions for a transaction name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "by_date": Object { + "aggs": Object { + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "sum_all_self_times": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + "total_transaction_breakdown_count": Object { + "sum": Object { + "field": "transaction.breakdown.count", + }, + }, + "types": Object { + "aggs": Object { + "subtypes": Object { + "aggs": Object { + "total_self_time_per_subtype": Object { + "sum": Object { + "field": "span.self_time.sum.us", + }, + }, + }, + "terms": Object { + "field": "span.subtype", + "missing": "", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "terms": Object { + "field": "span.type", + "order": Object { + "_count": "desc", + }, + "size": 20, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "transaction.type": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "transaction.name": "baz", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction charts 1`] = ` +Object { + "body": Object { + "aggs": Object { + "overall_avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "response_times": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "pct": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + 99, + ], + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "transaction_results": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "missing": "", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` +Object { + "body": Object { + "aggs": Object { + "overall_avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "response_times": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "pct": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + 99, + ], + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "transaction_results": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "missing": "", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "transaction.name": "bar", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "overall_avg_duration": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "response_times": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "pct": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + 99, + ], + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + "transaction_results": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "10800s", + "min_doc_count": 0, + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "missing": "", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + Object { + "term": Object { + "transaction.name": "bar", + }, + }, + Object { + "term": Object { + "transaction.type": "baz", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`transaction queries fetches transaction distribution 1`] = ` +Object { + "body": Object { + "aggs": Object { + "stats": Object { + "extended_stats": Object { + "field": "transaction.duration.us", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "baz", + }, + }, + Object { + "term": Object { + "transaction.name": "bar", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/queries.test.ts new file mode 100644 index 0000000000000..13cb1328fdd01 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/queries.test.ts @@ -0,0 +1,107 @@ +/* + * 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 { getTransactionBreakdown } from './breakdown'; +import { getTransactionCharts } from './charts'; +import { getTransactionDistribution } from './distribution'; +import { getTransaction } from './get_transaction'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('transaction queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches breakdown data for transactions', async () => { + mock = await inspectSearchParams(setup => + getTransactionBreakdown({ + serviceName: 'foo', + transactionType: 'bar', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches breakdown data for transactions for a transaction name', async () => { + mock = await inspectSearchParams(setup => + getTransactionBreakdown({ + serviceName: 'foo', + transactionType: 'bar', + transactionName: 'baz', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction charts', async () => { + mock = await inspectSearchParams(setup => + getTransactionCharts({ + serviceName: 'foo', + transactionName: undefined, + transactionType: undefined, + setup + }) + ); + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction charts for a transaction type', async () => { + mock = await inspectSearchParams(setup => + getTransactionCharts({ + serviceName: 'foo', + transactionName: 'bar', + transactionType: undefined, + setup + }) + ); + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction charts for a transaction type and transaction name', async () => { + mock = await inspectSearchParams(setup => + getTransactionCharts({ + serviceName: 'foo', + transactionName: 'bar', + transactionType: 'baz', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches transaction distribution', async () => { + mock = await inspectSearchParams(setup => + getTransactionDistribution({ + serviceName: 'foo', + transactionName: 'bar', + transactionType: 'baz', + traceId: 'qux', + transactionId: 'quz', + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches a transaction', async () => { + mock = await inspectSearchParams(setup => + getTransaction('foo', 'bar', setup) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..30e75f46ad5e7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ui filter queries fetches environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`ui filter queries fetches environments without a service name 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap new file mode 100644 index 0000000000000..594b363c1cf7b --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` +Object { + "body": Object { + "aggs": Object { + "host": Object { + "aggs": Object { + "by_terms": Object { + "aggs": Object { + "bucket_count": Object { + "cardinality": Object { + "field": "service.name", + }, + }, + }, + "terms": Object { + "field": "host.hostname", + "order": Object { + "_count": "desc", + }, + }, + }, + }, + "filter": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "transaction.result": Array [ + "2xx", + ], + }, + }, + ], + }, + }, + }, + "transactionResult": Object { + "aggs": Object { + "by_terms": Object { + "aggs": Object { + "bucket_count": Object { + "cardinality": Object { + "field": "service.name", + }, + }, + }, + "terms": Object { + "field": "transaction.result", + "order": Object { + "_count": "desc", + }, + }, + }, + }, + "filter": Object { + "bool": Object { + "filter": Array [], + }, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "service.environment": "prod", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts new file mode 100644 index 0000000000000..5d10a4ae27060 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -0,0 +1,77 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + CONTAINER_ID, + POD_NAME, + SERVICE_AGENT_NAME, + HOST_NAME, + TRANSACTION_RESULT +} from '../../../../common/elasticsearch_fieldnames'; + +const filtersByName = { + host: { + title: i18n.translate('xpack.apm.localFilters.titles.host', { + defaultMessage: 'Host' + }), + fieldName: HOST_NAME + }, + agentName: { + title: i18n.translate('xpack.apm.localFilters.titles.agentName', { + defaultMessage: 'Agent name' + }), + fieldName: SERVICE_AGENT_NAME + }, + containerId: { + title: i18n.translate('xpack.apm.localFilters.titles.containerId', { + defaultMessage: 'Container ID' + }), + fieldName: CONTAINER_ID + }, + podName: { + title: i18n.translate('xpack.apm.localFilters.titles.podName', { + defaultMessage: 'Pod' + }), + fieldName: POD_NAME + }, + transactionResult: { + title: i18n.translate('xpack.apm.localFilters.titles.transactionResult', { + defaultMessage: 'Transaction result' + }), + fieldName: TRANSACTION_RESULT + } +}; + +export type LocalUIFilterName = keyof typeof filtersByName; + +export interface LocalUIFilter { + name: LocalUIFilterName; + title: string; + fieldName: string; +} + +type LocalUIFilterMap = { + [key in LocalUIFilterName]: LocalUIFilter; +}; + +export const localUIFilterNames = Object.keys( + filtersByName +) as LocalUIFilterName[]; + +export const localUIFilters = localUIFilterNames.reduce( + (acc, key) => { + const field = filtersByName[key]; + + return { + ...acc, + [key]: { + ...field, + name: key + } + }; + }, + {} as LocalUIFilterMap +); diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts new file mode 100644 index 0000000000000..9c9a5c45f697c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/get_filter_aggregations.ts @@ -0,0 +1,72 @@ +/* + * 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 { omit } from 'lodash'; +import { Server } from 'hapi'; +import { Projection } from '../../../../common/projections/typings'; +import { UIFilters } from '../../../../typings/ui-filters'; +import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; +import { localUIFilters, LocalUIFilterName } from './config'; + +export const getFilterAggregations = async ({ + server, + uiFilters, + projection, + localFilterNames +}: { + server: Server; + uiFilters: UIFilters; + projection: Projection; + localFilterNames: LocalUIFilterName[]; +}) => { + const mappedFilters = localFilterNames.map(name => localUIFilters[name]); + + const aggs = await Promise.all( + mappedFilters.map(async field => { + const filter = await getUiFiltersES(server, omit(uiFilters, field.name)); + + const bucketCountAggregation = projection.body.aggs + ? { + aggs: { + bucket_count: { + cardinality: { + field: + projection.body.aggs[Object.keys(projection.body.aggs)[0]] + .terms.field + } + } + } + } + : {}; + + return { + [field.name]: { + filter: { + bool: { + filter + } + }, + aggs: { + by_terms: { + terms: { + field: field.fieldName, + order: { + _count: 'desc' + } + }, + ...bucketCountAggregation + } + } + } + }; + }) + ); + + const mergedAggregations = Object.assign({}, ...aggs) as Partial< + Record + >; + + return mergedAggregations; +}; diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts new file mode 100644 index 0000000000000..abc7f93adbe2a --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -0,0 +1,77 @@ +/* + * 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 { Server } from 'hapi'; +import { cloneDeep, sortByOrder } from 'lodash'; +import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { UIFilters } from '../../../../typings/ui-filters'; +import { Projection } from '../../../../common/projections/typings'; +import { PromiseReturnType } from '../../../../typings/common'; +import { getFilterAggregations } from './get_filter_aggregations'; +import { Setup } from '../../helpers/setup_request'; +import { localUIFilters, LocalUIFilterName } from './config'; + +export type LocalUIFiltersAPIResponse = PromiseReturnType< + typeof getLocalUIFilters +>; + +export async function getLocalUIFilters({ + server, + setup, + projection, + uiFilters, + localFilterNames +}: { + server: Server; + setup: Setup; + projection: Projection; + uiFilters: UIFilters; + localFilterNames: LocalUIFilterName[]; +}) { + const { client } = setup; + + const projectionWithoutAggs = cloneDeep(projection); + + delete projectionWithoutAggs.body.aggs; + + const filterAggregations = await getFilterAggregations({ + server, + uiFilters, + projection, + localFilterNames + }); + + const params = mergeProjection(projectionWithoutAggs, { + body: { + size: 0, + // help TS infer aggregations by making all aggregations required + aggs: filterAggregations as Required + } + }); + + const response = await client.search(params); + + return localFilterNames.map(key => { + const aggregations = response.aggregations[key]; + const filter = localUIFilters[key]; + + return { + ...filter, + options: sortByOrder( + aggregations.by_terms.buckets.map(bucket => { + return { + name: bucket.key, + count: + 'bucket_count' in bucket + ? bucket.bucket_count.value + : bucket.doc_count + }; + }), + 'count', + 'desc' + ) + }; + }); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts new file mode 100644 index 0000000000000..8bcde7c3af7a3 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { getLocalUIFilters } from './'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../../public/utils/testHelpers'; +import { getServicesProjection } from '../../../../common/projections/services'; + +describe('local ui filter queries', () => { + let mock: SearchParamsMock; + + beforeEach(() => { + jest.mock('../../helpers/convert_ui_filters/get_ui_filters_es', () => { + return []; + }); + }); + + afterEach(() => { + mock.teardown(); + }); + + it('fetches local ui filter aggregations', async () => { + mock = await inspectSearchParams(setup => + getLocalUIFilters({ + setup, + localFilterNames: ['transactionResult', 'host'], + projection: getServicesProjection({ setup }), + server: null as any, + uiFilters: { + transactionResult: ['2xx'] + } + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/queries.test.ts new file mode 100644 index 0000000000000..079ab64f32db3 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/queries.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { getEnvironments } from './get_environments'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../public/utils/testHelpers'; + +describe('ui filter queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches environments', async () => { + mock = await inspectSearchParams(setup => getEnvironments(setup, 'foo')); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches environments without a service name', async () => { + mock = await inspectSearchParams(setup => getEnvironments(setup)); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts index 9f6903c9840a6..28a44bd3e9e37 100644 --- a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts @@ -5,11 +5,24 @@ */ import Boom from 'boom'; -import Joi from 'joi'; +import Joi, { Schema } from 'joi'; import { InternalCoreSetup } from 'src/core/server'; +import { omit } from 'lodash'; import { withDefaultValidators } from '../lib/helpers/input_validation'; -import { setupRequest } from '../lib/helpers/setup_request'; +import { setupRequest, Setup } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; +import { PROJECTION, Projection } from '../../common/projections/typings'; +import { + LocalUIFilterName, + localUIFilterNames +} from '../lib/ui_filters/local_ui_filters/config'; +import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; +import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; +import { getServicesProjection } from '../../common/projections/services'; +import { getTransactionGroupsProjection } from '../../common/projections/transaction_groups'; +import { getMetricsProjection } from '../../common/projections/metrics'; +import { getErrorGroupsProjection } from '../../common/projections/errors'; +import { getTransactionsProjection } from '../../common/projections/transactions'; const defaultErrorHandler = (err: Error) => { // eslint-disable-next-line @@ -38,4 +51,161 @@ export function initUIFiltersApi(core: InternalCoreSetup) { return getEnvironments(setup, serviceName).catch(defaultErrorHandler); } }); + + const createLocalFiltersEndpoint = ({ + name, + getProjection, + validators + }: { + name: PROJECTION; + getProjection: ({ + setup, + query + }: { + setup: Setup; + query: Record; + }) => Projection; + validators?: Record; + }) => { + server.route({ + method: 'GET', + path: `/api/apm/ui_filters/local_filters/${name}`, + options: { + validate: { + query: withDefaultValidators({ + filterNames: Joi.array() + .items(localUIFilterNames) + .required(), + ...validators + }) + }, + tags: ['access:apm'] + }, + handler: async req => { + const setup = await setupRequest(req); + + const { uiFilters, filterNames } = (req.query as unknown) as { + uiFilters: string; + filterNames: LocalUIFilterName[]; + }; + + const parsedUiFilters = JSON.parse(uiFilters); + + const projection = getProjection({ + query: req.query as Record, + setup: { + ...setup, + uiFiltersES: await getUiFiltersES( + req.server, + omit(parsedUiFilters, filterNames) + ) + } + }); + + return getLocalUIFilters({ + server: req.server, + projection, + setup, + uiFilters: parsedUiFilters, + localFilterNames: filterNames + }).catch(defaultErrorHandler); + } + }); + }; + + createLocalFiltersEndpoint({ + name: PROJECTION.SERVICES, + getProjection: ({ setup }) => { + return getServicesProjection({ setup }); + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.TRACES, + getProjection: ({ setup }) => { + return getTransactionGroupsProjection({ + setup, + options: { type: 'top_traces' } + }); + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.TRANSACTION_GROUPS, + getProjection: ({ setup, query }) => { + const { transactionType, serviceName, transactionName } = query as { + transactionType: string; + serviceName: string; + transactionName?: string; + }; + return getTransactionGroupsProjection({ + setup, + options: { + type: 'top_transactions', + transactionType, + serviceName, + transactionName + } + }); + }, + validators: { + serviceName: Joi.string().required(), + transactionType: Joi.string().required(), + transactionName: Joi.string() + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.TRANSACTIONS, + getProjection: ({ setup, query }) => { + const { transactionType, serviceName, transactionName } = query as { + transactionType: string; + serviceName: string; + transactionName: string; + }; + return getTransactionsProjection({ + setup, + transactionType, + serviceName, + transactionName + }); + }, + validators: { + serviceName: Joi.string().required(), + transactionType: Joi.string().required(), + transactionName: Joi.string().required() + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.METRICS, + getProjection: ({ setup, query }) => { + const { serviceName } = query as { + serviceName: string; + }; + return getMetricsProjection({ + setup, + serviceName + }); + }, + validators: { + serviceName: Joi.string().required() + } + }); + + createLocalFiltersEndpoint({ + name: PROJECTION.ERROR_GROUPS, + getProjection: ({ setup, query }) => { + const { serviceName } = query as { + serviceName: string; + }; + return getErrorGroupsProjection({ + setup, + serviceName + }); + }, + validators: { + serviceName: Joi.string().required() + } + }); } diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index f424ea21ab34b..150ec70286acc 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -25,7 +25,10 @@ declare module 'elasticsearch' { | 'min' | 'percentiles' | 'sum' - | 'extended_stats'; + | 'extended_stats' + | 'filter' + | 'filters' + | 'cardinality'; type AggOptions = AggregationOptionMap & { [key: string]: any; @@ -40,6 +43,10 @@ declare module 'elasticsearch' { }; }; + type SubAggregation = T extends { aggs: any } + ? AggregationResultMap + : {}; + // eslint-disable-next-line @typescript-eslint/prefer-interface type BucketAggregation = { buckets: Array< @@ -47,9 +54,20 @@ declare module 'elasticsearch' { key: KeyType; key_as_string: string; doc_count: number; - } & (SubAggregationMap extends { aggs: any } - ? AggregationResultMap - : {}) + } & (SubAggregation) + >; + }; + + type FilterAggregation = { + doc_count: number; + } & SubAggregation; + + // eslint-disable-next-line @typescript-eslint/prefer-interface + type FiltersAggregation = { + buckets: Array< + { + doc_count: number; + } & SubAggregation >; }; @@ -105,6 +123,11 @@ declare module 'elasticsearch' { lower: number | null; }; }; + filter: FilterAggregation; + filters: FiltersAggregation; + cardinality: { + value: number; + }; }[AggregationType & keyof AggregationOption[AggregationName]]; } >; diff --git a/x-pack/legacy/plugins/apm/typings/ui-filters.ts b/x-pack/legacy/plugins/apm/typings/ui-filters.ts index 6566f5c9471a2..0c62ef3b1c962 100644 --- a/x-pack/legacy/plugins/apm/typings/ui-filters.ts +++ b/x-pack/legacy/plugins/apm/typings/ui-filters.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface UIFilters { +import { LocalUIFilterName } from '../server/lib/ui_filters/local_ui_filters/config'; + +export type UIFilters = { kuery?: string; environment?: string; -} +} & { [key in LocalUIFilterName]?: string[] }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 92633cd614da2..22735bda893c7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3905,7 +3905,6 @@ "xpack.apm.transactions.chart.averageLabel": "平均", "xpack.apm.transactionsTable.95thPercentileColumnLabel": "95 パーセンタイル", "xpack.apm.transactionsTable.avgDurationColumnLabel": "平均期間", - "xpack.apm.transactionsTable.filterByTypeLabel": "タイプでフィルタリング", "xpack.apm.transactionsTable.impactColumnLabel": "インパクト", "xpack.apm.transactionsTable.nameColumnLabel": "名前", "xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel": "1 分あたりのトランザクション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ab185bd9b8843..84a797e2b185b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3905,7 +3905,6 @@ "xpack.apm.transactions.chart.averageLabel": "平均", "xpack.apm.transactionsTable.95thPercentileColumnLabel": "第 95 个百分位", "xpack.apm.transactionsTable.avgDurationColumnLabel": "平均持续时间", - "xpack.apm.transactionsTable.filterByTypeLabel": "按类型筛选", "xpack.apm.transactionsTable.impactColumnLabel": "影响", "xpack.apm.transactionsTable.nameColumnLabel": "名称", "xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel": "每分钟事务数",