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`,
|
std_dev: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-extendedstats-aggregation.html`,
|
||||||
sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`,
|
sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`,
|
||||||
top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`,
|
top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`,
|
||||||
|
top_metrics: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-metrics.html`,
|
||||||
},
|
},
|
||||||
runtimeFields: {
|
runtimeFields: {
|
||||||
overview: `${ELASTICSEARCH_DOCS}runtime.html`,
|
overview: `${ELASTICSEARCH_DOCS}runtime.html`,
|
||||||
|
|
|
@ -36,6 +36,7 @@ export const getAggTypes = () => ({
|
||||||
{ name: METRIC_TYPES.PERCENTILES, fn: metrics.getPercentilesMetricAgg },
|
{ name: METRIC_TYPES.PERCENTILES, fn: metrics.getPercentilesMetricAgg },
|
||||||
{ name: METRIC_TYPES.PERCENTILE_RANKS, fn: metrics.getPercentileRanksMetricAgg },
|
{ name: METRIC_TYPES.PERCENTILE_RANKS, fn: metrics.getPercentileRanksMetricAgg },
|
||||||
{ name: METRIC_TYPES.TOP_HITS, fn: metrics.getTopHitMetricAgg },
|
{ 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.DERIVATIVE, fn: metrics.getDerivativeMetricAgg },
|
||||||
{ name: METRIC_TYPES.CUMULATIVE_SUM, fn: metrics.getCumulativeSumMetricAgg },
|
{ name: METRIC_TYPES.CUMULATIVE_SUM, fn: metrics.getCumulativeSumMetricAgg },
|
||||||
{ name: METRIC_TYPES.MOVING_FN, fn: metrics.getMovingAvgMetricAgg },
|
{ name: METRIC_TYPES.MOVING_FN, fn: metrics.getMovingAvgMetricAgg },
|
||||||
|
@ -109,4 +110,5 @@ export const getAggTypesFunctions = () => [
|
||||||
metrics.aggStdDeviation,
|
metrics.aggStdDeviation,
|
||||||
metrics.aggSum,
|
metrics.aggSum,
|
||||||
metrics.aggTopHit,
|
metrics.aggTopHit,
|
||||||
|
metrics.aggTopMetrics,
|
||||||
];
|
];
|
||||||
|
|
|
@ -95,6 +95,7 @@ describe('Aggs service', () => {
|
||||||
"percentiles",
|
"percentiles",
|
||||||
"percentile_ranks",
|
"percentile_ranks",
|
||||||
"top_hits",
|
"top_hits",
|
||||||
|
"top_metrics",
|
||||||
"derivative",
|
"derivative",
|
||||||
"cumulative_sum",
|
"cumulative_sum",
|
||||||
"moving_avg",
|
"moving_avg",
|
||||||
|
@ -147,6 +148,7 @@ describe('Aggs service', () => {
|
||||||
"percentiles",
|
"percentiles",
|
||||||
"percentile_ranks",
|
"percentile_ranks",
|
||||||
"top_hits",
|
"top_hits",
|
||||||
|
"top_metrics",
|
||||||
"derivative",
|
"derivative",
|
||||||
"cumulative_sum",
|
"cumulative_sum",
|
||||||
"moving_avg",
|
"moving_avg",
|
||||||
|
|
|
@ -56,3 +56,5 @@ export * from './sum_fn';
|
||||||
export * from './sum';
|
export * from './sum';
|
||||||
export * from './top_hit_fn';
|
export * from './top_hit_fn';
|
||||||
export * from './top_hit';
|
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 = [
|
const metricAggFilter = [
|
||||||
'!top_hits',
|
'!top_hits',
|
||||||
|
'!top_metrics',
|
||||||
'!percentiles',
|
'!percentiles',
|
||||||
'!percentile_ranks',
|
'!percentile_ranks',
|
||||||
'!median',
|
'!median',
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type';
|
||||||
|
|
||||||
const metricAggFilter: string[] = [
|
const metricAggFilter: string[] = [
|
||||||
'!top_hits',
|
'!top_hits',
|
||||||
|
'!top_metrics',
|
||||||
'!percentiles',
|
'!percentiles',
|
||||||
'!percentile_ranks',
|
'!percentile_ranks',
|
||||||
'!median',
|
'!median',
|
||||||
|
|
|
@ -22,6 +22,7 @@ export interface MetricAggParam<TMetricAggConfig extends AggConfig>
|
||||||
extends AggParamType<TMetricAggConfig> {
|
extends AggParamType<TMetricAggConfig> {
|
||||||
filterFieldTypes?: FieldTypes;
|
filterFieldTypes?: FieldTypes;
|
||||||
onlyAggregatable?: boolean;
|
onlyAggregatable?: boolean;
|
||||||
|
scriptable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metricType = 'metrics';
|
const metricType = 'metrics';
|
||||||
|
|
|
@ -27,6 +27,7 @@ export enum METRIC_TYPES {
|
||||||
SERIAL_DIFF = 'serial_diff',
|
SERIAL_DIFF = 'serial_diff',
|
||||||
SUM = 'sum',
|
SUM = 'sum',
|
||||||
TOP_HITS = 'top_hits',
|
TOP_HITS = 'top_hits',
|
||||||
|
TOP_METRICS = 'top_metrics',
|
||||||
PERCENTILES = 'percentiles',
|
PERCENTILES = 'percentiles',
|
||||||
PERCENTILE_RANKS = 'percentile_ranks',
|
PERCENTILE_RANKS = 'percentile_ranks',
|
||||||
STD_DEV = 'std_dev',
|
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.filterFieldTypes = config.filterFieldTypes || '*';
|
||||||
this.onlyAggregatable = config.onlyAggregatable !== false;
|
this.onlyAggregatable = config.onlyAggregatable !== false;
|
||||||
|
this.scriptable = config.scriptable !== false;
|
||||||
this.filterField = config.filterField;
|
this.filterField = config.filterField;
|
||||||
|
|
||||||
if (!config.write) {
|
if (!config.write) {
|
||||||
|
|
|
@ -93,6 +93,8 @@ import {
|
||||||
import { AggParamsSampler } from './buckets/sampler';
|
import { AggParamsSampler } from './buckets/sampler';
|
||||||
import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler';
|
import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler';
|
||||||
import { AggParamsSignificantText } from './buckets/significant_text';
|
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 { IAggConfig, AggConfigSerialized } from './agg_config';
|
||||||
export type { CreateAggConfigParams, IAggConfigs } from './agg_configs';
|
export type { CreateAggConfigParams, IAggConfigs } from './agg_configs';
|
||||||
|
@ -187,6 +189,7 @@ export interface AggParamsMapping {
|
||||||
[METRIC_TYPES.PERCENTILES]: AggParamsPercentiles;
|
[METRIC_TYPES.PERCENTILES]: AggParamsPercentiles;
|
||||||
[METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff;
|
[METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff;
|
||||||
[METRIC_TYPES.TOP_HITS]: AggParamsTopHit;
|
[METRIC_TYPES.TOP_HITS]: AggParamsTopHit;
|
||||||
|
[METRIC_TYPES.TOP_METRICS]: AggParamsTopMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -229,4 +232,5 @@ export interface AggFunctionsMapping {
|
||||||
aggStdDeviation: ReturnType<typeof aggStdDeviation>;
|
aggStdDeviation: ReturnType<typeof aggStdDeviation>;
|
||||||
aggSum: ReturnType<typeof aggSum>;
|
aggSum: ReturnType<typeof aggSum>;
|
||||||
aggTopHit: ReturnType<typeof aggTopHit>;
|
aggTopHit: ReturnType<typeof aggTopHit>;
|
||||||
|
aggTopMetrics: ReturnType<typeof aggTopMetrics>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ describe('AggsService - public', () => {
|
||||||
service.setup(setupDeps);
|
service.setup(setupDeps);
|
||||||
const start = service.start(startDeps);
|
const start = service.start(startDeps);
|
||||||
expect(start.types.getAll().buckets.length).toBe(16);
|
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', () => {
|
test('registers custom agg types', () => {
|
||||||
|
@ -71,7 +71,7 @@ describe('AggsService - public', () => {
|
||||||
const start = service.start(startDeps);
|
const start = service.start(startDeps);
|
||||||
expect(start.types.getAll().buckets.length).toBe(17);
|
expect(start.types.getAll().buckets.length).toBe(17);
|
||||||
expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true);
|
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);
|
expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -67,6 +67,11 @@ const metrics = {
|
||||||
sortField: controls.TopSortFieldParamEditor,
|
sortField: controls.TopSortFieldParamEditor,
|
||||||
sortOrder: controls.OrderParamEditor,
|
sortOrder: controls.OrderParamEditor,
|
||||||
},
|
},
|
||||||
|
[METRIC_TYPES.TOP_METRICS]: {
|
||||||
|
field: controls.FieldParamEditor,
|
||||||
|
sortField: controls.TopSortFieldParamEditor,
|
||||||
|
sortOrder: controls.OrderParamEditor,
|
||||||
|
},
|
||||||
[METRIC_TYPES.PERCENTILES]: {
|
[METRIC_TYPES.PERCENTILES]: {
|
||||||
percents: controls.PercentilesEditor,
|
percents: controls.PercentilesEditor,
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,14 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { useAvailableOptions, useFallbackMetric, useValidation } from './utils';
|
import { useAvailableOptions, useFallbackMetric, useValidation } from './utils';
|
||||||
import { AggParamEditorProps } from '../agg_param_props';
|
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 EMPTY_VALUE = 'EMPTY_VALUE';
|
||||||
const DEFAULT_OPTIONS = [{ text: '', value: EMPTY_VALUE, hidden: true }];
|
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({
|
await kibanaServer.uiSettings.replace({
|
||||||
'dateFormat:tz': 'Australia/North',
|
'dateFormat:tz': 'Australia/North',
|
||||||
defaultIndex: 'logstash-*',
|
defaultIndex: 'logstash-*',
|
||||||
|
'bfetch:disableCompression': true, // makes it easier to debug while developing tests
|
||||||
});
|
});
|
||||||
await browser.setWindowSize(1300, 900);
|
await browser.setWindowSize(1300, 900);
|
||||||
await PageObjects.common.navigateToApp('settings');
|
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_sampler'));
|
||||||
loadTestFile(require.resolve('./esaggs_significanttext'));
|
loadTestFile(require.resolve('./esaggs_significanttext'));
|
||||||
loadTestFile(require.resolve('./esaggs_rareterms'));
|
loadTestFile(require.resolve('./esaggs_rareterms'));
|
||||||
|
loadTestFile(require.resolve('./esaggs_topmetrics'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue