mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[AggConfigs] Add TopMetrics agg (#125936)
This commit is contained in:
parent
a79562a67e
commit
eca203ce73
19 changed files with 679 additions and 3 deletions
|
@ -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`,
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -15,6 +15,7 @@ import { parentPipelineAggWriter } from './parent_pipeline_agg_writer';
|
|||
|
||||
const metricAggFilter = [
|
||||
'!top_hits',
|
||||
'!top_metrics',
|
||||
'!percentiles',
|
||||
'!percentile_ranks',
|
||||
'!median',
|
||||
|
|
|
@ -13,6 +13,7 @@ import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type';
|
|||
|
||||
const metricAggFilter: string[] = [
|
||||
'!top_hits',
|
||||
'!top_metrics',
|
||||
'!percentiles',
|
||||
'!percentile_ranks',
|
||||
'!median',
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface MetricAggParam<TMetricAggConfig extends AggConfig>
|
|||
extends AggParamType<TMetricAggConfig> {
|
||||
filterFieldTypes?: FieldTypes;
|
||||
onlyAggregatable?: boolean;
|
||||
scriptable?: boolean;
|
||||
}
|
||||
|
||||
const metricType = 'metrics';
|
||||
|
|
|
@ -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',
|
||||
|
|
194
src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts
Normal file
194
src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts
Normal file
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
155
src/plugins/data/common/search/aggs/metrics/top_metrics.ts
Normal file
155
src/plugins/data/common/search/aggs/metrics/top_metrics.ts
Normal file
|
@ -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<string, estypes.AggregationsTopMetricsAggregate | undefined>) {
|
||||
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;
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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 }');
|
||||
});
|
||||
});
|
||||
});
|
106
src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts
Normal file
106
src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts
Normal file
|
@ -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<typeof METRIC_TYPES.TOP_METRICS>;
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -93,6 +93,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';
|
||||
|
@ -187,6 +189,7 @@ export interface AggParamsMapping {
|
|||
[METRIC_TYPES.PERCENTILES]: AggParamsPercentiles;
|
||||
[METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff;
|
||||
[METRIC_TYPES.TOP_HITS]: AggParamsTopHit;
|
||||
[METRIC_TYPES.TOP_METRICS]: AggParamsTopMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -229,4 +232,5 @@ export interface AggFunctionsMapping {
|
|||
aggStdDeviation: ReturnType<typeof aggStdDeviation>;
|
||||
aggSum: ReturnType<typeof aggSum>;
|
||||
aggTopHit: ReturnType<typeof aggTopHit>;
|
||||
aggTopMetrics: ReturnType<typeof aggTopMetrics>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 }];
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue