Skip to content

Commit

Permalink
[data.search.aggs] Add support for rate aggregation (#143487)
Browse files Browse the repository at this point in the history
## Summary

Resolves #131386.

Adds support for the rate aggregation. Usage may look something like the
following:

```ts
const aggs = [
  {
    type: 'date_histogram',
    params: {
      field: '@timestamp',
      interval: '1h',
    },
  },
  {
    type: 'rate',
    params: {
      field: 'bytes', // optional
      unit: 'hour',
    },
  },
];
const aggsDsl = data.search.aggs.createAggConfigs(dataView, aggs).toDsl();

// Which generates the following DSL:
{
  "1": {
    "date_histogram": {
      "field": "@timestamp",
      "calendar_interval": "1h",
      "time_zone": "America/Phoenix",
      "min_doc_count": 1
    },
    "aggs": {
      "2": {
        "rate": {
          "field": "bytes",
          "unit": "hour"
        }
      }
    }
  }
}
```

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

Co-authored-by: Peter Pisljar <peter.pisljar@elastic.co>
  • Loading branch information
lukasolson and ppisljar authored Dec 15, 2022
1 parent c99f40f commit 5a6bf1d
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/plugins/data/common/search/aggs/agg_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const getAggTypes = () => ({
{ name: METRIC_TYPES.VALUE_COUNT, fn: metrics.getValueCountMetricAgg },
{ name: METRIC_TYPES.PERCENTILES, fn: metrics.getPercentilesMetricAgg },
{ name: METRIC_TYPES.PERCENTILE_RANKS, fn: metrics.getPercentileRanksMetricAgg },
{ name: METRIC_TYPES.RATE, fn: metrics.getRateMetricAgg },
{ name: METRIC_TYPES.TOP_HITS, fn: metrics.getTopHitMetricAgg },
{ name: METRIC_TYPES.TOP_METRICS, fn: metrics.getTopMetricsMetricAgg },
{ name: METRIC_TYPES.DERIVATIVE, fn: metrics.getDerivativeMetricAgg },
Expand Down Expand Up @@ -112,6 +113,7 @@ export const getAggTypesFunctions = () => [
metrics.aggMovingAvg,
metrics.aggPercentileRanks,
metrics.aggPercentiles,
metrics.aggRate,
metrics.aggSerialDiff,
metrics.aggStdDeviation,
metrics.aggSum,
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/data/common/search/aggs/aggs_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('Aggs service', () => {
"value_count",
"percentiles",
"percentile_ranks",
"rate",
"top_hits",
"top_metrics",
"derivative",
Expand Down Expand Up @@ -140,6 +141,7 @@ describe('Aggs service', () => {
"value_count",
"percentiles",
"percentile_ranks",
"rate",
"top_hits",
"top_metrics",
"derivative",
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/data/common/search/aggs/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export * from './percentile_ranks_fn';
export * from './percentile_ranks';
export * from './percentiles_fn';
export * from './percentiles';
export * from './rate_fn';
export * from './rate';
export * from './single_percentile_rank_fn';
export * from './single_percentile_rank';
export * from './serial_diff_fn';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ export enum METRIC_TYPES {
TOP_METRICS = 'top_metrics',
PERCENTILES = 'percentiles',
PERCENTILE_RANKS = 'percentile_ranks',
RATE = 'rate',
STD_DEV = 'std_dev',
}
104 changes: 104 additions & 0 deletions src/plugins/data/common/search/aggs/metrics/rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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 { aggRateFnName } from './rate_fn';
import { MetricAggType } from './metric_agg_type';
import { METRIC_TYPES } from './metric_agg_types';
import { KBN_FIELD_TYPES } from '../../..';
import { BaseAggParams } from '../types';

const rateTitle = i18n.translate('data.search.aggs.metrics.rateTitle', {
defaultMessage: 'Rate',
});

export interface AggParamsRate extends BaseAggParams {
unit: string;
field?: string;
}

export const getRateMetricAgg = () => {
return new MetricAggType({
name: METRIC_TYPES.RATE,
expressionName: aggRateFnName,
title: rateTitle,
valueType: 'number',
makeLabel: (aggConfig) => {
return i18n.translate('data.search.aggs.metrics.rateLabel', {
defaultMessage: 'Rate of {field} per {unit}',
values: { field: aggConfig.getFieldDisplayName(), unit: aggConfig.getParam('unit') },
});
},
params: [
{
name: 'field',
type: 'field',
required: false,
filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM],
},
{
name: 'unit',
type: 'string',
displayName: i18n.translate('data.search.aggs.metrics.rate.unit.displayName', {
defaultMessage: 'Unit',
}),
required: true,
options: [
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.second', {
defaultMessage: 'Second',
}),
value: 'second',
},
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.minute', {
defaultMessage: 'Minute',
}),
value: 'minute',
},
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.hour', {
defaultMessage: 'Hour',
}),
value: 'hour',
},
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.day', {
defaultMessage: 'Day',
}),
value: 'day',
},
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.week', {
defaultMessage: 'Week',
}),
value: 'week',
},
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.month', {
defaultMessage: 'Month',
}),
value: 'month',
},
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.quarter', {
defaultMessage: 'Quarter',
}),
value: 'quarter',
},
{
text: i18n.translate('data.search.aggs.metrics.rate.unit.year', {
defaultMessage: 'Year',
}),
value: 'year',
},
],
},
],
});
};
75 changes: 75 additions & 0 deletions src/plugins/data/common/search/aggs/metrics/rate_fn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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 { aggRate } from './rate_fn';

describe('agg_expression_functions', () => {
describe('aggRate', () => {
const fn = functionWrapper(aggRate());

test('without field', () => {
const actual = fn({
unit: 'second',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": undefined,
"json": undefined,
"timeShift": undefined,
"unit": "second",
},
"schema": undefined,
"type": "rate",
},
}
`);
});

test('with field', () => {
const actual = fn({
field: 'bytes',
unit: 'second',
});
expect(actual).toMatchInlineSnapshot(`
Object {
"type": "agg_type",
"value": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customLabel": undefined,
"field": "bytes",
"json": undefined,
"timeShift": undefined,
"unit": "second",
},
"schema": undefined,
"type": "rate",
},
}
`);
});

test('correctly parses json string argument', () => {
const actual = fn({
field: 'machine.os.keyword',
unit: 'month',
json: '{ "foo": true }',
});

expect(actual.value.params.json).toMatchInlineSnapshot(`"{ \\"foo\\": true }"`);
});
});
});
101 changes: 101 additions & 0 deletions src/plugins/data/common/search/aggs/metrics/rate_fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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 '@kbn/expressions-plugin/common';
import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '..';

export const aggRateFnName = 'aggRate';

type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.RATE>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggRateFnName,
Input,
AggArgs,
Output
>;

export const aggRate = (): FunctionDefinition => ({
name: aggRateFnName,
help: i18n.translate('data.search.aggs.function.metrics.rate.help', {
defaultMessage: 'Generates a serialized agg config for a Rate agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.rate.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.metrics.rate.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.rate.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
field: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.rate.field.help', {
defaultMessage: 'Field to use for this aggregation',
}),
},
unit: {
types: ['string'],
required: true,
options: ['second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'],
help: i18n.translate('data.search.aggs.metrics.rate.unit.help', {
defaultMessage: 'Unit to use for this aggregation',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.rate.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.rate.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
timeShift: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.timeShift.help', {
defaultMessage:
'Shift the time range for the metric by a set time, for example 1h or 7d. "previous" will use the closest time range from the date histogram or time range filter.',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;

return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: METRIC_TYPES.RATE,
params: {
...rest,
},
},
};
},
});
1 change: 1 addition & 0 deletions src/plugins/data/common/search/aggs/param_types/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class FieldParamType extends BaseParamType {
const field = aggConfig.getField();

if (!field) {
if (config.required === false) return;
throw new TypeError(
i18n.translate('data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage', {
defaultMessage: '{fieldParameter} is a required parameter',
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/data/common/search/aggs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
AggParamsPercentileRanks,
AggParamsPercentiles,
AggParamsRange,
AggParamsRate,
AggParamsSerialDiff,
AggParamsSignificantTerms,
AggParamsStdDeviation,
Expand Down Expand Up @@ -209,6 +210,7 @@ interface SerializedAggParamsMapping {
[METRIC_TYPES.MOVING_FN]: AggParamsMovingAvgSerialized;
[METRIC_TYPES.PERCENTILE_RANKS]: AggParamsPercentileRanks;
[METRIC_TYPES.PERCENTILES]: AggParamsPercentiles;
[METRIC_TYPES.RATE]: AggParamsRate;
[METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiffSerialized;
[METRIC_TYPES.TOP_HITS]: AggParamsTopHitSerialized;
[METRIC_TYPES.TOP_METRICS]: AggParamsTopMetricsSerialized;
Expand Down Expand Up @@ -254,6 +256,7 @@ export interface AggParamsMapping {
[METRIC_TYPES.MOVING_FN]: AggParamsMovingAvg;
[METRIC_TYPES.PERCENTILE_RANKS]: AggParamsPercentileRanks;
[METRIC_TYPES.PERCENTILES]: AggParamsPercentiles;
[METRIC_TYPES.RATE]: AggParamsRate;
[METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff;
[METRIC_TYPES.TOP_HITS]: AggParamsTopHit;
[METRIC_TYPES.TOP_METRICS]: AggParamsTopMetrics;
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/data/public/search/aggs/aggs_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,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(26);
expect(start.types.getAll().metrics.length).toBe(27);
});

test('registers custom agg types', () => {
Expand All @@ -70,7 +70,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(27);
expect(start.types.getAll().metrics.length).toBe(28);
expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true);
});
});
Expand Down

0 comments on commit 5a6bf1d

Please sign in to comment.