diff --git a/x-pack/plugins/apm/common/correlations/field_stats_types.ts b/x-pack/plugins/apm/common/correlations/field_stats_types.ts
index 50dc7919fbd00..41f7e3c3c6649 100644
--- a/x-pack/plugins/apm/common/correlations/field_stats_types.ts
+++ b/x-pack/plugins/apm/common/correlations/field_stats_types.ts
@@ -8,9 +8,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { CorrelationsParams } from './types';
-export interface FieldStatsCommonRequestParams extends CorrelationsParams {
- samplerShardSize: number;
-}
+export type FieldStatsCommonRequestParams = CorrelationsParams;
export interface Field {
fieldName: string;
@@ -55,3 +53,5 @@ export type FieldStats =
| NumericFieldStats
| KeywordFieldStats
| BooleanFieldStats;
+
+export type FieldValueFieldStats = TopValuesStats;
diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx
index f1d0d194749c5..d7043ea669a03 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx
@@ -11,14 +11,11 @@ import {
EuiFlexItem,
EuiPopover,
EuiPopoverTitle,
- EuiSpacer,
- EuiText,
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
-import React, { Fragment, useState } from 'react';
+import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
import { FieldStats } from '../../../../../common/correlations/field_stats_types';
import { OnAddFilter, TopValues } from './top_values';
import { useTheme } from '../../../../hooks/use_theme';
@@ -97,27 +94,11 @@ export function CorrelationsContextPopover({
{infoIsOpen ? (
- <>
-
- {topValueStats.topValuesSampleSize !== undefined && (
-
-
-
-
-
-
- )}
- >
+
) : null}
);
diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx
index 05b4f6d56fa45..fbf33899a2de2 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx
@@ -12,11 +12,21 @@ import {
EuiProgress,
EuiSpacer,
EuiToolTip,
+ EuiText,
+ EuiHorizontalRule,
+ EuiLoadingSpinner,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FieldStats } from '../../../../../common/correlations/field_stats_types';
+import numeral from '@elastic/numeral';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ FieldStats,
+ TopValueBucket,
+} from '../../../../../common/correlations/field_stats_types';
import { asPercent } from '../../../../../common/utils/formatters';
import { useTheme } from '../../../../hooks/use_theme';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
+import { useFetchParams } from '../use_fetch_params';
export type OnAddFilter = ({
fieldName,
@@ -28,23 +38,179 @@ export type OnAddFilter = ({
include: boolean;
}) => void;
-interface Props {
+interface TopValueProps {
+ progressBarMax: number;
+ barColor: string;
+ value: TopValueBucket;
+ isHighlighted: boolean;
+ fieldName: string;
+ onAddFilter?: OnAddFilter;
+ valueText?: string;
+ reverseLabel?: boolean;
+}
+export function TopValue({
+ progressBarMax,
+ barColor,
+ value,
+ isHighlighted,
+ fieldName,
+ onAddFilter,
+ valueText,
+ reverseLabel = false,
+}: TopValueProps) {
+ const theme = useTheme();
+ return (
+
+
+
+ {value.key}
+
+ }
+ className="eui-textTruncate"
+ aria-label={value.key.toString()}
+ valueText={valueText}
+ labelProps={
+ isHighlighted
+ ? {
+ style: { fontWeight: 'bold' },
+ }
+ : undefined
+ }
+ />
+
+ {fieldName !== undefined &&
+ value.key !== undefined &&
+ onAddFilter !== undefined ? (
+ <>
+ {
+ onAddFilter({
+ fieldName,
+ fieldValue:
+ typeof value.key === 'number'
+ ? value.key.toString()
+ : value.key,
+ include: true,
+ });
+ }}
+ aria-label={i18n.translate(
+ 'xpack.apm.correlations.fieldContextPopover.addFilterAriaLabel',
+ {
+ defaultMessage: 'Filter for {fieldName}: "{value}"',
+ values: { fieldName, value: value.key },
+ }
+ )}
+ data-test-subj={`apmFieldContextTopValuesAddFilterButton-${value.key}-${value.key}`}
+ style={{
+ minHeight: 'auto',
+ width: theme.eui.euiSizeL,
+ paddingRight: 2,
+ paddingLeft: 2,
+ paddingTop: 0,
+ paddingBottom: 0,
+ }}
+ />
+ {
+ onAddFilter({
+ fieldName,
+ fieldValue:
+ typeof value.key === 'number'
+ ? value.key.toString()
+ : value.key,
+ include: false,
+ });
+ }}
+ aria-label={i18n.translate(
+ 'xpack.apm.correlations.fieldContextPopover.removeFilterAriaLabel',
+ {
+ defaultMessage: 'Filter out {fieldName}: "{value}"',
+ values: { fieldName, value: value.key },
+ }
+ )}
+ data-test-subj={`apmFieldContextTopValuesExcludeFilterButton-${value.key}-${value.key}`}
+ style={{
+ minHeight: 'auto',
+ width: theme.eui.euiSizeL,
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingRight: 2,
+ paddingLeft: 2,
+ }}
+ />
+ >
+ ) : null}
+
+ );
+}
+
+interface TopValuesProps {
topValueStats: FieldStats;
compressed?: boolean;
onAddFilter?: OnAddFilter;
fieldValue?: string | number;
}
-export function TopValues({ topValueStats, onAddFilter, fieldValue }: Props) {
+export function TopValues({
+ topValueStats,
+ onAddFilter,
+ fieldValue,
+}: TopValuesProps) {
const { topValues, topValuesSampleSize, count, fieldName } = topValueStats;
const theme = useTheme();
- if (!Array.isArray(topValues) || topValues.length === 0) return null;
+ const idxToHighlight = Array.isArray(topValues)
+ ? topValues.findIndex((value) => value.key === fieldValue)
+ : null;
+
+ const params = useFetchParams();
+ const { data: fieldValueStats, status } = useFetcher(
+ (callApmApi) => {
+ if (
+ idxToHighlight === -1 &&
+ fieldName !== undefined &&
+ fieldValue !== undefined
+ ) {
+ return callApmApi({
+ endpoint: 'GET /internal/apm/correlations/field_value_stats',
+ params: {
+ query: {
+ ...params,
+ fieldName,
+ fieldValue,
+ },
+ },
+ });
+ }
+ },
+ [params, fieldName, fieldValue, idxToHighlight]
+ );
+ if (
+ !Array.isArray(topValues) ||
+ topValues?.length === 0 ||
+ fieldValue === undefined
+ )
+ return null;
const sampledSize =
typeof topValuesSampleSize === 'string'
? parseInt(topValuesSampleSize, 10)
: topValuesSampleSize;
+
const progressBarMax = sampledSize ?? count;
return (
-
-
-
- {value.key}
-
- }
- className="eui-textTruncate"
- aria-label={value.key.toString()}
- valueText={valueText}
- labelProps={
- isHighlighted
- ? {
- style: { fontWeight: 'bold' },
- }
- : undefined
- }
- />
-
- {fieldName !== undefined &&
- value.key !== undefined &&
- onAddFilter !== undefined ? (
- <>
- {
- onAddFilter({
- fieldName,
- fieldValue:
- typeof value.key === 'number'
- ? value.key.toString()
- : value.key,
- include: true,
- });
- }}
- aria-label={i18n.translate(
- 'xpack.apm.correlations.fieldContextPopover.addFilterAriaLabel',
- {
- defaultMessage: 'Filter for {fieldName}: "{value}"',
- values: { fieldName, value: value.key },
- }
- )}
- data-test-subj={`apmFieldContextTopValuesAddFilterButton-${value.key}-${value.key}`}
- style={{
- minHeight: 'auto',
- width: theme.eui.euiSizeL,
- paddingRight: 2,
- paddingLeft: 2,
- paddingTop: 0,
- paddingBottom: 0,
- }}
- />
- {
- onAddFilter({
- fieldName,
- fieldValue:
- typeof value.key === 'number'
- ? value.key.toString()
- : value.key,
- include: false,
- });
- }}
- aria-label={i18n.translate(
- 'xpack.apm.correlations.fieldContextPopover.removeFilterAriaLabel',
- {
- defaultMessage: 'Filter out {fieldName}: "{value}"',
- values: { fieldName, value: value.key },
- }
- )}
- data-test-subj={`apmFieldContextTopValuesExcludeFilterButton-${value.key}-${value.key}`}
- style={{
- minHeight: 'auto',
- width: theme.eui.euiSizeL,
- paddingTop: 0,
- paddingBottom: 0,
- paddingRight: 2,
- paddingLeft: 2,
- }}
- />
- >
- ) : null}
-
+
>
);
})}
+
+ {idxToHighlight === -1 && (
+ <>
+
+
+
+
+
+ {status === FETCH_STATUS.SUCCESS &&
+ Array.isArray(fieldValueStats?.topValues) ? (
+ fieldValueStats?.topValues.map((value) => {
+ const valueText =
+ progressBarMax !== undefined
+ ? asPercent(value.doc_count, progressBarMax)
+ : undefined;
+
+ return (
+
+ );
+ })
+ ) : (
+
+
+
+ )}
+ >
+ )}
+
+ {topValueStats.topValuesSampleSize !== undefined && (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.apm.correlations.fieldContextPopover.calculatedFromSampleDescription',
+ {
+ defaultMessage:
+ 'Calculated from sample of {sampleSize} documents',
+ values: { sampleSize: topValueStats.topValuesSampleSize },
+ }
+ )}
+
+ >
+ )}
);
}
diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts
index c936e626a5599..a41e3370c1063 100644
--- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts
@@ -7,8 +7,6 @@
import { ElasticsearchClient } from 'kibana/server';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-
-import { buildSamplerAggregation } from '../../utils/field_stats_utils';
import { FieldValuePair } from '../../../../../common/correlations/types';
import {
FieldStatsCommonRequestParams,
@@ -25,7 +23,7 @@ export const getBooleanFieldStatsRequest = (
): estypes.SearchRequest => {
const query = getQueryWithParams({ params, termFilters });
- const { index, samplerShardSize } = params;
+ const { index } = params;
const size = 0;
const aggs: Aggs = {
@@ -42,14 +40,13 @@ export const getBooleanFieldStatsRequest = (
const searchBody = {
query,
- aggs: {
- sample: buildSamplerAggregation(aggs, samplerShardSize),
- },
+ aggs,
};
return {
index,
size,
+ track_total_hits: false,
body: searchBody,
};
};
@@ -67,19 +64,17 @@ export const fetchBooleanFieldStats = async (
);
const { body } = await esClient.search(request);
const aggregations = body.aggregations as {
- sample: {
- sampled_value_count: estypes.AggregationsFiltersBucketItemKeys;
- sampled_values: estypes.AggregationsTermsAggregate;
- };
+ sampled_value_count: estypes.AggregationsFiltersBucketItemKeys;
+ sampled_values: estypes.AggregationsTermsAggregate;
};
const stats: BooleanFieldStats = {
fieldName: field.fieldName,
- count: aggregations?.sample.sampled_value_count.doc_count ?? 0,
+ count: aggregations?.sampled_value_count.doc_count ?? 0,
};
const valueBuckets: TopValueBucket[] =
- aggregations?.sample.sampled_values?.buckets ?? [];
+ aggregations?.sampled_values?.buckets ?? [];
valueBuckets.forEach((bucket) => {
stats[`${bucket.key.toString()}Count`] = bucket.doc_count;
});
diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts
index 2775d755c9907..30bebc4c24774 100644
--- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts
@@ -20,7 +20,6 @@ const params = {
includeFrozen: false,
environment: ENVIRONMENT_ALL.value,
kuery: '',
- samplerShardSize: 5000,
};
export const getExpectedQuery = (aggs: any) => {
@@ -46,6 +45,7 @@ export const getExpectedQuery = (aggs: any) => {
},
index: 'apm-*',
size: 0,
+ track_total_hits: false,
};
};
@@ -55,28 +55,16 @@ describe('field_stats', () => {
const req = getNumericFieldStatsRequest(params, 'url.path');
const expectedAggs = {
- sample: {
- aggs: {
- sampled_field_stats: {
- aggs: { actual_stats: { stats: { field: 'url.path' } } },
- filter: { exists: { field: 'url.path' } },
- },
- sampled_percentiles: {
- percentiles: {
- field: 'url.path',
- keyed: false,
- percents: [50],
- },
- },
- sampled_top: {
- terms: {
- field: 'url.path',
- order: { _count: 'desc' },
- size: 10,
- },
- },
+ sampled_field_stats: {
+ aggs: { actual_stats: { stats: { field: 'url.path' } } },
+ filter: { exists: { field: 'url.path' } },
+ },
+ sampled_top: {
+ terms: {
+ field: 'url.path',
+ order: { _count: 'desc' },
+ size: 10,
},
- sampler: { shard_size: 5000 },
},
};
expect(req).toEqual(getExpectedQuery(expectedAggs));
@@ -87,13 +75,8 @@ describe('field_stats', () => {
const req = getKeywordFieldStatsRequest(params, 'url.path');
const expectedAggs = {
- sample: {
- sampler: { shard_size: 5000 },
- aggs: {
- sampled_top: {
- terms: { field: 'url.path', size: 10, order: { _count: 'desc' } },
- },
- },
+ sampled_top: {
+ terms: { field: 'url.path', size: 10 },
},
};
expect(req).toEqual(getExpectedQuery(expectedAggs));
@@ -104,15 +87,10 @@ describe('field_stats', () => {
const req = getBooleanFieldStatsRequest(params, 'url.path');
const expectedAggs = {
- sample: {
- sampler: { shard_size: 5000 },
- aggs: {
- sampled_value_count: {
- filter: { exists: { field: 'url.path' } },
- },
- sampled_values: { terms: { field: 'url.path', size: 2 } },
- },
+ sampled_value_count: {
+ filter: { exists: { field: 'url.path' } },
},
+ sampled_values: { terms: { field: 'url.path', size: 2 } },
};
expect(req).toEqual(getExpectedQuery(expectedAggs));
});
diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts
new file mode 100644
index 0000000000000..0fa508eff508c
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ElasticsearchClient } from 'kibana/server';
+import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { FieldValuePair } from '../../../../../common/correlations/types';
+import {
+ FieldStatsCommonRequestParams,
+ FieldValueFieldStats,
+ Aggs,
+ TopValueBucket,
+} from '../../../../../common/correlations/field_stats_types';
+import { getQueryWithParams } from '../get_query_with_params';
+
+export const getFieldValueFieldStatsRequest = (
+ params: FieldStatsCommonRequestParams,
+ field?: FieldValuePair
+): estypes.SearchRequest => {
+ const query = getQueryWithParams({ params });
+
+ const { index } = params;
+
+ const size = 0;
+ const aggs: Aggs = {
+ filtered_count: {
+ filter: {
+ term: {
+ [`${field?.fieldName}`]: field?.fieldValue,
+ },
+ },
+ },
+ };
+
+ const searchBody = {
+ query,
+ aggs,
+ };
+
+ return {
+ index,
+ size,
+ track_total_hits: false,
+ body: searchBody,
+ };
+};
+
+export const fetchFieldValueFieldStats = async (
+ esClient: ElasticsearchClient,
+ params: FieldStatsCommonRequestParams,
+ field: FieldValuePair
+): Promise => {
+ const request = getFieldValueFieldStatsRequest(params, field);
+
+ const { body } = await esClient.search(request);
+ const aggregations = body.aggregations as {
+ filtered_count: estypes.AggregationsFiltersBucketItemKeys;
+ };
+ const topValues: TopValueBucket[] = [
+ {
+ key: field.fieldValue,
+ doc_count: aggregations.filtered_count.doc_count,
+ },
+ ];
+
+ const stats = {
+ fieldName: field.fieldName,
+ topValues,
+ topValuesSampleSize: aggregations.filtered_count.doc_count ?? 0,
+ };
+
+ return stats;
+};
diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts
index 8b41f7662679c..513252ee65e11 100644
--- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts
@@ -8,10 +8,7 @@
import { ElasticsearchClient } from 'kibana/server';
import { chunk } from 'lodash';
import { ES_FIELD_TYPES } from '@kbn/field-types';
-import {
- FieldValuePair,
- CorrelationsParams,
-} from '../../../../../common/correlations/types';
+import { FieldValuePair } from '../../../../../common/correlations/types';
import {
FieldStats,
FieldStatsCommonRequestParams,
@@ -23,7 +20,7 @@ import { fetchBooleanFieldStats } from './get_boolean_field_stats';
export const fetchFieldsStats = async (
esClient: ElasticsearchClient,
- params: CorrelationsParams,
+ fieldStatsParams: FieldStatsCommonRequestParams,
fieldsToSample: string[],
termFilters?: FieldValuePair[]
): Promise<{ stats: FieldStats[]; errors: any[] }> => {
@@ -33,14 +30,10 @@ export const fetchFieldsStats = async (
if (fieldsToSample.length === 0) return { stats, errors };
const respMapping = await esClient.fieldCaps({
- ...getRequestBase(params),
+ ...getRequestBase(fieldStatsParams),
fields: fieldsToSample,
});
- const fieldStatsParams: FieldStatsCommonRequestParams = {
- ...params,
- samplerShardSize: 5000,
- };
const fieldStatsPromises = Object.entries(respMapping.body.fields)
.map(([key, value], idx) => {
const field: FieldValuePair = { fieldName: key, fieldValue: '' };
diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts
index c64bbc6678779..16ba4f24f5e93 100644
--- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts
@@ -14,7 +14,6 @@ import {
Aggs,
TopValueBucket,
} from '../../../../../common/correlations/field_stats_types';
-import { buildSamplerAggregation } from '../../utils/field_stats_utils';
import { getQueryWithParams } from '../get_query_with_params';
export const getKeywordFieldStatsRequest = (
@@ -24,7 +23,7 @@ export const getKeywordFieldStatsRequest = (
): estypes.SearchRequest => {
const query = getQueryWithParams({ params, termFilters });
- const { index, samplerShardSize } = params;
+ const { index } = params;
const size = 0;
const aggs: Aggs = {
@@ -32,23 +31,19 @@ export const getKeywordFieldStatsRequest = (
terms: {
field: fieldName,
size: 10,
- order: {
- _count: 'desc',
- },
},
},
};
const searchBody = {
query,
- aggs: {
- sample: buildSamplerAggregation(aggs, samplerShardSize),
- },
+ aggs,
};
return {
index,
size,
+ track_total_hits: false,
body: searchBody,
};
};
@@ -66,19 +61,16 @@ export const fetchKeywordFieldStats = async (
);
const { body } = await esClient.search(request);
const aggregations = body.aggregations as {
- sample: {
- sampled_top: estypes.AggregationsTermsAggregate;
- };
+ sampled_top: estypes.AggregationsTermsAggregate;
};
- const topValues: TopValueBucket[] =
- aggregations?.sample.sampled_top?.buckets ?? [];
+ const topValues: TopValueBucket[] = aggregations?.sampled_top?.buckets ?? [];
const stats = {
fieldName: field.fieldName,
topValues,
topValuesSampleSize: topValues.reduce(
(acc, curr) => acc + curr.doc_count,
- aggregations.sample.sampled_top?.sum_other_doc_count ?? 0
+ aggregations.sampled_top?.sum_other_doc_count ?? 0
),
};
diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts
index 21e6559fdda25..197ed66c4fe70 100644
--- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts
@@ -6,7 +6,7 @@
*/
import { ElasticsearchClient } from 'kibana/server';
-import { find, get } from 'lodash';
+import { get } from 'lodash';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
NumericFieldStats,
@@ -16,10 +16,6 @@ import {
} from '../../../../../common/correlations/field_stats_types';
import { FieldValuePair } from '../../../../../common/correlations/types';
import { getQueryWithParams } from '../get_query_with_params';
-import { buildSamplerAggregation } from '../../utils/field_stats_utils';
-
-// Only need 50th percentile for the median
-const PERCENTILES = [50];
export const getNumericFieldStatsRequest = (
params: FieldStatsCommonRequestParams,
@@ -29,9 +25,8 @@ export const getNumericFieldStatsRequest = (
const query = getQueryWithParams({ params, termFilters });
const size = 0;
- const { index, samplerShardSize } = params;
+ const { index } = params;
- const percents = PERCENTILES;
const aggs: Aggs = {
sampled_field_stats: {
filter: { exists: { field: fieldName } },
@@ -41,13 +36,6 @@ export const getNumericFieldStatsRequest = (
},
},
},
- sampled_percentiles: {
- percentiles: {
- field: fieldName,
- percents,
- keyed: false,
- },
- },
sampled_top: {
terms: {
field: fieldName,
@@ -61,14 +49,13 @@ export const getNumericFieldStatsRequest = (
const searchBody = {
query,
- aggs: {
- sample: buildSamplerAggregation(aggs, samplerShardSize),
- },
+ aggs,
};
return {
index,
size,
+ track_total_hits: false,
body: searchBody,
};
};
@@ -87,19 +74,15 @@ export const fetchNumericFieldStats = async (
const { body } = await esClient.search(request);
const aggregations = body.aggregations as {
- sample: {
- sampled_top: estypes.AggregationsTermsAggregate;
- sampled_percentiles: estypes.AggregationsHdrPercentilesAggregate;
- sampled_field_stats: {
- doc_count: number;
- actual_stats: estypes.AggregationsStatsAggregate;
- };
+ sampled_top: estypes.AggregationsTermsAggregate;
+ sampled_field_stats: {
+ doc_count: number;
+ actual_stats: estypes.AggregationsStatsAggregate;
};
};
- const docCount = aggregations?.sample.sampled_field_stats?.doc_count ?? 0;
- const fieldStatsResp =
- aggregations?.sample.sampled_field_stats?.actual_stats ?? {};
- const topValues = aggregations?.sample.sampled_top?.buckets ?? [];
+ const docCount = aggregations?.sampled_field_stats?.doc_count ?? 0;
+ const fieldStatsResp = aggregations?.sampled_field_stats?.actual_stats ?? {};
+ const topValues = aggregations?.sampled_top?.buckets ?? [];
const stats: NumericFieldStats = {
fieldName: field.fieldName,
@@ -110,20 +93,9 @@ export const fetchNumericFieldStats = async (
topValues,
topValuesSampleSize: topValues.reduce(
(acc: number, curr: TopValueBucket) => acc + curr.doc_count,
- aggregations.sample.sampled_top?.sum_other_doc_count ?? 0
+ aggregations.sampled_top?.sum_other_doc_count ?? 0
),
};
- if (stats.count !== undefined && stats.count > 0) {
- const percentiles = aggregations?.sample.sampled_percentiles.values ?? [];
- const medianPercentile: { value: number; key: number } | undefined = find(
- percentiles,
- {
- key: 50,
- }
- );
- stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0;
- }
-
return stats;
};
diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts b/x-pack/plugins/apm/server/routes/correlations/queries/index.ts
index 548127eb7647d..d2a86a20bd5c6 100644
--- a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/queries/index.ts
@@ -16,3 +16,4 @@ export { fetchTransactionDurationCorrelation } from './query_correlation';
export { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram';
export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps';
export { fetchTransactionDurationRanges } from './query_ranges';
+export { fetchFieldValueFieldStats } from './field_stats/get_field_value_stats';
diff --git a/x-pack/plugins/apm/server/routes/correlations/route.ts b/x-pack/plugins/apm/server/routes/correlations/route.ts
index b02a6fbc6b7a6..377fedf9d1813 100644
--- a/x-pack/plugins/apm/server/routes/correlations/route.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/route.ts
@@ -19,6 +19,7 @@ import {
fetchSignificantCorrelations,
fetchTransactionDurationFieldCandidates,
fetchTransactionDurationFieldValuePairs,
+ fetchFieldValueFieldStats,
} from './queries';
import { fetchFieldsStats } from './queries/field_stats/get_fields_stats';
@@ -77,12 +78,12 @@ const fieldStatsRoute = createApmServerRoute({
transactionName: t.string,
transactionType: t.string,
}),
- environmentRt,
- kueryRt,
- rangeRt,
t.type({
fieldsToSample: t.array(t.string),
}),
+ environmentRt,
+ kueryRt,
+ rangeRt,
]),
}),
options: { tags: ['access:apm'] },
@@ -112,6 +113,51 @@ const fieldStatsRoute = createApmServerRoute({
},
});
+const fieldValueStatsRoute = createApmServerRoute({
+ endpoint: 'GET /internal/apm/correlations/field_value_stats',
+ params: t.type({
+ query: t.intersection([
+ t.partial({
+ serviceName: t.string,
+ transactionName: t.string,
+ transactionType: t.string,
+ }),
+ environmentRt,
+ kueryRt,
+ rangeRt,
+ t.type({
+ fieldName: t.string,
+ fieldValue: t.union([t.string, t.number]),
+ }),
+ ]),
+ }),
+ options: { tags: ['access:apm'] },
+ handler: async (resources) => {
+ const { context } = resources;
+ if (!isActivePlatinumLicense(context.licensing.license)) {
+ throw Boom.forbidden(INVALID_LICENSE);
+ }
+
+ const { indices } = await setupRequest(resources);
+ const esClient = resources.context.core.elasticsearch.client.asCurrentUser;
+
+ const { fieldName, fieldValue, ...params } = resources.params.query;
+
+ return withApmSpan(
+ 'get_correlations_field_value_stats',
+ async () =>
+ await fetchFieldValueFieldStats(
+ esClient,
+ {
+ ...params,
+ index: indices.transaction,
+ },
+ { fieldName, fieldValue }
+ )
+ );
+ },
+});
+
const fieldValuePairsRoute = createApmServerRoute({
endpoint: 'POST /internal/apm/correlations/field_value_pairs',
params: t.type({
@@ -252,5 +298,6 @@ export const correlationsRouteRepository = createApmServerRouteRepository()
.add(pValuesRoute)
.add(fieldCandidatesRoute)
.add(fieldStatsRoute)
+ .add(fieldValueStatsRoute)
.add(fieldValuePairsRoute)
.add(significantCorrelationsRoute);
diff --git a/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts b/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts
index 7f98f771c50e2..a60622583781b 100644
--- a/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts
+++ b/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-
/*
* Contains utility functions for building and processing queries.
*/
@@ -38,22 +36,3 @@ export function buildBaseFilterCriteria(
return filterCriteria;
}
-
-// Wraps the supplied aggregations in a sampler aggregation.
-// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation)
-// of less than 1 indicates no sampling, and the aggs are returned as-is.
-export function buildSamplerAggregation(
- aggs: any,
- samplerShardSize: number
-): estypes.AggregationsAggregationContainer {
- if (samplerShardSize < 1) {
- return aggs;
- }
-
- return {
- sampler: {
- shard_size: samplerShardSize,
- },
- aggs,
- };
-}