From 81bc3ff48e29e832de60889d99331885cd530059 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 2 Mar 2022 11:42:31 +0100 Subject: [PATCH] top metrics --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + .../data/common/search/aggs/agg_types.ts | 2 + .../common/search/aggs/aggs_service.test.ts | 2 + .../data/common/search/aggs/metrics/index.ts | 2 + .../metrics/lib/parent_pipeline_agg_helper.ts | 1 + .../lib/sibling_pipeline_agg_helper.ts | 1 + .../search/aggs/metrics/metric_agg_type.ts | 1 + .../search/aggs/metrics/metric_agg_types.ts | 1 + .../search/aggs/metrics/top_metrics.test.ts | 194 ++++++++++++++++++ .../common/search/aggs/metrics/top_metrics.ts | 155 ++++++++++++++ .../aggs/metrics/top_metrics_fn.test.ts | 79 +++++++ .../search/aggs/metrics/top_metrics_fn.ts | 106 ++++++++++ .../common/search/aggs/param_types/field.ts | 1 + src/plugins/data/common/search/aggs/types.ts | 4 + .../public/search/aggs/aggs_service.test.ts | 4 +- .../public/components/agg_params_map.ts | 5 + .../public/components/controls/metric_agg.tsx | 9 +- .../run_pipeline/esaggs_topmetrics.ts | 112 ++++++++++ .../test_suites/run_pipeline/index.ts | 2 + 19 files changed, 679 insertions(+), 3 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts create mode 100644 test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d73760b280d496..03948af6379104 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -196,6 +196,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { std_dev: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-extendedstats-aggregation.html`, sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`, top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`, + top_metrics: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-metrics.html`, }, runtimeFields: { overview: `${ELASTICSEARCH_DOCS}runtime.html`, diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index 01ccd401c07acc..d7750c48016cd5 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -36,6 +36,7 @@ export const getAggTypes = () => ({ { name: METRIC_TYPES.PERCENTILES, fn: metrics.getPercentilesMetricAgg }, { name: METRIC_TYPES.PERCENTILE_RANKS, fn: metrics.getPercentileRanksMetricAgg }, { name: METRIC_TYPES.TOP_HITS, fn: metrics.getTopHitMetricAgg }, + { name: METRIC_TYPES.TOP_METRICS, fn: metrics.getTopMetricsMetricAgg }, { name: METRIC_TYPES.DERIVATIVE, fn: metrics.getDerivativeMetricAgg }, { name: METRIC_TYPES.CUMULATIVE_SUM, fn: metrics.getCumulativeSumMetricAgg }, { name: METRIC_TYPES.MOVING_FN, fn: metrics.getMovingAvgMetricAgg }, @@ -109,4 +110,5 @@ export const getAggTypesFunctions = () => [ metrics.aggStdDeviation, metrics.aggSum, metrics.aggTopHit, + metrics.aggTopMetrics, ]; diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index 998b8bf286b52b..ade19daad9e9dd 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -95,6 +95,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", @@ -147,6 +148,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", diff --git a/src/plugins/data/common/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts index d37b74a1a28aef..4d80e363251001 100644 --- a/src/plugins/data/common/search/aggs/metrics/index.ts +++ b/src/plugins/data/common/search/aggs/metrics/index.ts @@ -56,3 +56,5 @@ export * from './sum_fn'; export * from './sum'; export * from './top_hit_fn'; export * from './top_hit'; +export * from './top_metrics'; +export * from './top_metrics_fn'; diff --git a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 478b8309272e39..1fe703313218d8 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -15,6 +15,7 @@ import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; const metricAggFilter = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index f8c903b8cfe420..243a119847a2cf 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -13,6 +13,7 @@ import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; const metricAggFilter: string[] = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts index 6ddb0fdd9410d4..5237c1ecffe584 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts @@ -22,6 +22,7 @@ export interface MetricAggParam extends AggParamType { filterFieldTypes?: FieldTypes; onlyAggregatable?: boolean; + scriptable?: boolean; } const metricType = 'metrics'; diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts index a308153b3816b7..eed6d0a378fc2d 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts @@ -27,6 +27,7 @@ export enum METRIC_TYPES { SERIAL_DIFF = 'serial_diff', SUM = 'sum', TOP_HITS = 'top_hits', + TOP_METRICS = 'top_metrics', PERCENTILES = 'percentiles', PERCENTILE_RANKS = 'percentile_ranks', STD_DEV = 'std_dev', diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts new file mode 100644 index 00000000000000..9bf5f581aa0a4a --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts @@ -0,0 +1,194 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getTopMetricsMetricAgg } from './top_metrics'; +import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { IMetricAggConfig } from './metric_agg_type'; +import { KBN_FIELD_TYPES } from '../../../../common'; + +describe('Top metrics metric', () => { + let aggConfig: IMetricAggConfig; + + const init = ({ + fieldName = 'field', + fieldType = KBN_FIELD_TYPES.NUMBER, + sortFieldName = 'sortField', + sortFieldType = KBN_FIELD_TYPES.NUMBER, + sortOrder = 'desc', + size = 1, + }: any) => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: fieldName, + displayName: fieldName, + type: fieldType, + }; + + const sortField = { + name: sortFieldName, + displayName: sortFieldName, + type: sortFieldType, + }; + + const params = { + size, + field: field.name, + sortField: sortField.name, + sortOrder: { + value: sortOrder, + }, + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: (name: string) => { + if (name === sortFieldName) return sortField; + if (name === fieldName) return field; + return null; + }, + filter: () => [field, sortField], + }, + } as any; + + const aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: '1', + type: 'top_metrics', + schema: 'metric', + params, + }, + ], + { typesRegistry } + ); + + // Grab the aggConfig off the vis (we don't actually use the vis for anything else) + aggConfig = aggConfigs.aggs[0] as IMetricAggConfig; + }; + + it('should return a label prefixed with Last if sorting in descending order', () => { + init({ fieldName: 'bytes', sortFieldName: '@timestamp' }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'Last "bytes" value by "@timestamp"' + ); + }); + + it('should return a label prefixed with First if sorting in ascending order', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First "bytes" value by "@timestamp"' + ); + }); + + it('should return a label with size if larger then 1', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First 3 "bytes" values by "@timestamp"' + ); + }); + + it('should return a fieldName in getValueBucketPath', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().getValueBucketPath(aggConfig)).toEqual('1[bytes]'); + }); + + it('produces the expected expression ast', () => { + init({ fieldName: 'machine.os', sortFieldName: '@timestamp' }); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "machine.os", + ], + "id": Array [ + "1", + ], + "schema": Array [ + "metric", + ], + "size": Array [ + 1, + ], + "sortField": Array [ + "@timestamp", + ], + "sortOrder": Array [ + "desc", + ], + }, + "function": "aggTopMetrics", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + describe('gets value from top metrics bucket', () => { + it('should return null if there is no hits', () => { + const bucket = { + '1': { + top: [], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(null); + }); + + it('should return a single value if there is a single hit', () => { + const bucket = { + '1': { + top: [{ sort: [3], metrics: { bytes: 1024 } }], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(1024); + }); + + it('should return an array of values if there is a multiple results', () => { + const bucket = { + '1': { + top: [ + { sort: [3], metrics: { bytes: 1024 } }, + { sort: [2], metrics: { bytes: 512 } }, + { sort: [1], metrics: { bytes: 256 } }, + ], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toEqual([1024, 512, 256]); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts new file mode 100644 index 00000000000000..2079925e0435b1 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts @@ -0,0 +1,155 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import _ from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { i18n } from '@kbn/i18n'; +import { aggTopMetricsFnName } from './top_metrics_fn'; +import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../common'; +import { BaseAggParams } from '../types'; + +export interface AggParamsTopMetrics extends BaseAggParams { + field: string; + sortField?: string; + sortOrder?: 'desc' | 'asc'; + size?: number; +} + +export const getTopMetricsMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.TOP_METRICS, + expressionName: aggTopMetricsFnName, + title: i18n.translate('data.search.aggs.metrics.topMetricsTitle', { + defaultMessage: 'Top metrics', + }), + makeLabel(aggConfig) { + const isDescOrder = aggConfig.getParam('sortOrder').value === 'desc'; + const size = aggConfig.getParam('size'); + const field = aggConfig.getParam('field'); + const sortField = aggConfig.getParam('sortField'); + + if (isDescOrder) { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.descWithSizeLabel', { + defaultMessage: `Last {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.descNoSizeLabel', { + defaultMessage: `Last "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } else { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascWithSizeLabel', { + defaultMessage: `First {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascNoSizeLabel', { + defaultMessage: `First "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } + }, + params: [ + { + name: 'field', + type: 'field', + scriptable: false, + filterFieldTypes: [ + KBN_FIELD_TYPES.STRING, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.BOOLEAN, + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.DATE, + ], + write(agg, output) { + const field = agg.getParam('field'); + output.params.metrics = { field: field.name }; + }, + }, + { + name: 'size', + default: 1, + }, + { + name: 'sortField', + type: 'field', + scriptable: false, + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + default(agg: IMetricAggConfig) { + return agg.getIndexPattern().timeFieldName; + }, + write: _.noop, // prevent default write, it is handled below + }, + { + name: 'sortOrder', + type: 'optioned', + default: 'desc', + options: [ + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.descendingLabel', { + defaultMessage: 'Descending', + }), + value: 'desc', + }, + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.ascendingLabel', { + defaultMessage: 'Ascending', + }), + value: 'asc', + }, + ], + write(agg, output) { + const sortField = agg.params.sortField; + const sortOrder = agg.params.sortOrder; + + if (sortField && sortOrder) { + output.params.sort = { + [sortField.name]: sortOrder.value, + }; + } else { + output.params.sort = '_score'; + } + }, + }, + ], + // override is needed to support top_metrics as an orderAgg of terms agg + getValueBucketPath(agg) { + const field = agg.getParam('field').name; + return `${agg.id}[${field}]`; + }, + getValue(agg, aggregate: Record) { + const metricFieldName = agg.getParam('field').name; + const results = aggregate[agg.id]?.top.map((result) => result.metrics[metricFieldName]) ?? []; + + if (results.length === 0) return null; + if (results.length === 1) return results[0]; + return results; + }, + }); +}; diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts new file mode 100644 index 00000000000000..848fccda283faa --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts @@ -0,0 +1,79 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggTopMetrics } from './top_metrics_fn'; + +describe('agg_expression_functions', () => { + describe('aggTopMetrics', () => { + const fn = functionWrapper(aggTopMetrics()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": undefined, + "sortField": undefined, + "sortOrder": undefined, + }, + "schema": undefined, + "type": "top_metrics", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + sortOrder: 'asc', + size: 6, + sortField: 'bytes', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": 6, + "sortField": "bytes", + "sortOrder": "asc", + }, + "schema": "whatever", + "type": "top_metrics", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual('{ "foo": true }'); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts new file mode 100644 index 00000000000000..6fe9ba97fe4483 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts @@ -0,0 +1,106 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; + +export const aggTopMetricsFnName = 'aggTopMetrics'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggTopMetricsFnName, + Input, + AggArgs, + Output +>; + +export const aggTopMetrics = (): FunctionDefinition => ({ + name: aggTopMetricsFnName, + help: i18n.translate('data.search.aggs.function.metrics.topMetrics.help', { + defaultMessage: 'Generates a serialized aggregation configuration for Top metrics.', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + size: { + types: ['number'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.size.help', { + defaultMessage: 'Number of top values to retrieve', + }), + }, + sortOrder: { + types: ['string'], + options: ['desc', 'asc'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortOrder.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + sortField: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortField.help', { + defaultMessage: 'Field to order results by', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.json.help', { + defaultMessage: 'Advanced JSON to include when the aggregation is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.TOP_METRICS, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index 940fdafd548753..b56787121f7248 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -43,6 +43,7 @@ export class FieldParamType extends BaseParamType { this.filterFieldTypes = config.filterFieldTypes || '*'; this.onlyAggregatable = config.onlyAggregatable !== false; + this.scriptable = config.scriptable !== false; this.filterField = config.filterField; if (!config.write) { diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index 34d773b0ba518e..10535df45bf354 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -95,6 +95,8 @@ import { import { AggParamsSampler } from './buckets/sampler'; import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; import { AggParamsSignificantText } from './buckets/significant_text'; +import { AggParamsTopMetrics } from './metrics/top_metrics'; +import { aggTopMetrics } from './metrics/top_metrics_fn'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; @@ -194,6 +196,7 @@ export interface AggParamsMapping { [METRIC_TYPES.PERCENTILES]: AggParamsPercentiles; [METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff; [METRIC_TYPES.TOP_HITS]: AggParamsTopHit; + [METRIC_TYPES.TOP_METRICS]: AggParamsTopMetrics; } /** @@ -236,4 +239,5 @@ export interface AggFunctionsMapping { aggStdDeviation: ReturnType; aggSum: ReturnType; aggTopHit: ReturnType; + aggTopMetrics: ReturnType; } diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index b0e6e0327e6544..907f92a07cceb3 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -54,7 +54,7 @@ describe('AggsService - public', () => { service.setup(setupDeps); const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(16); - expect(start.types.getAll().metrics.length).toBe(23); + expect(start.types.getAll().metrics.length).toBe(24); }); test('registers custom agg types', () => { @@ -71,7 +71,7 @@ describe('AggsService - public', () => { const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(17); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); - expect(start.types.getAll().metrics.length).toBe(24); + expect(start.types.getAll().metrics.length).toBe(25); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); }); }); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index a61df61f2316c6..283e1d7511b750 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -67,6 +67,11 @@ const metrics = { sortField: controls.TopSortFieldParamEditor, sortOrder: controls.OrderParamEditor, }, + [METRIC_TYPES.TOP_METRICS]: { + field: controls.FieldParamEditor, + sortField: controls.TopSortFieldParamEditor, + sortOrder: controls.OrderParamEditor, + }, [METRIC_TYPES.PERCENTILES]: { percents: controls.PercentilesEditor, }, diff --git a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx index 1f844b50424746..2888d399bc0148 100644 --- a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx @@ -13,7 +13,14 @@ import { i18n } from '@kbn/i18n'; import { useAvailableOptions, useFallbackMetric, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; -const aggFilter = ['!top_hits', '!percentiles', '!percentile_ranks', '!median', '!std_dev']; +const aggFilter = [ + '!top_hits', + '!top_metrics', + '!percentiles', + '!percentile_ranks', + '!median', + '!std_dev', +]; const EMPTY_VALUE = 'EMPTY_VALUE'; const DEFAULT_OPTIONS = [{ text: '', value: EMPTY_VALUE, hidden: true }]; diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts new file mode 100644 index 00000000000000..4f43709ba4a7e0 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts @@ -0,0 +1,112 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function ({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + + describe('esaggs_topmetrics', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + const timeRange = { + from: '2015-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + + describe('aggTopMetrics', () => { + it('can execute aggTopMetrics', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=3 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(r['col-1-2'].length).to.be(3); + expect( + r['col-1-2'].forEach((metric) => { + expect(typeof metric).to.be('number'); + }) + ); + }); + }); + + it('can execute aggTopMetrics with different sortOrder and size', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(typeof r['col-1-2']).to.be('number'); + }); + }); + + it('can use aggTopMetrics as an orderAgg of aggTerms', async () => { + const expressionSortBytesAsc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesAsc = await expectExpression( + 'sortBytesAsc', + expressionSortBytesAsc + ).getResponse(); + + const expressionSortBytesDesc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesDesc = await expectExpression( + 'sortBytesDesc', + expressionSortBytesDesc + ).getResponse(); + + expect(resultSortBytesAsc.rows.length).to.be(1); + expect(resultSortBytesAsc.rows[0]['col-0-1']).to.be('jpg'); + + expect(resultSortBytesDesc.rows.length).to.be(1); + expect(resultSortBytesDesc.rows[0]['col-0-1']).to.be('php'); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 97387fc0a965fc..e24563a5918eb8 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', defaultIndex: 'logstash-*', + 'bfetch:disableCompression': true, // makes it easier to debug while developing tests }); await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('settings'); @@ -47,5 +48,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./esaggs_sampler')); loadTestFile(require.resolve('./esaggs_significanttext')); loadTestFile(require.resolve('./esaggs_rareterms')); + loadTestFile(require.resolve('./esaggs_topmetrics')); }); }