From 0ae549726c08370a3f9295893812ecd32ff37182 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Tue, 10 Mar 2020 14:44:06 -0700 Subject: [PATCH] [TSVB] Add Rate to Aggregations --- .../public/components/aggs/agg_select.js | 6 + .../public/components/aggs/rate.js | 130 ++++++++++++++++++ .../public/components/lib/agg_to_component.js | 2 + .../vis_type_timeseries/common/agg_lookup.js | 1 + .../common/calculate_label.js | 5 + .../request_processors/series/index.js | 2 + .../request_processors/series/rate.js | 68 +++++++++ .../request_processors/series/rate.test.js | 95 +++++++++++++ .../request_processors/table/index.js | 2 + .../vis_data/request_processors/table/rate.js | 35 +++++ 10 files changed, 346 insertions(+) create mode 100644 src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/rate.js create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.js create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.test.js create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/rate.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js index f93dee14d0eed3..de1d6eb6a08693 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js @@ -115,6 +115,12 @@ const metricAggs = [ }), value: 'variance', }, + { + label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.rateLabel', { + defaultMessage: 'Rate', + }), + value: 'rate', + }, ]; const pipelineAggs = [ diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/rate.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/rate.js new file mode 100644 index 00000000000000..a553e67a8a3350 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/rate.js @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import { AggSelect } from './agg_select'; +import { FieldSelect } from './field_select'; +import { AggRow } from './agg_row'; +import { createChangeHandler } from '../lib/create_change_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { createTextHandler } from '../lib/create_text_handler'; +import { + htmlIdGenerator, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiFieldText, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; + +export const RateAgg = props => { + const defaults = { unit: '' }; + const model = { ...defaults, ...props.model }; + + const handleChange = createChangeHandler(props.onChange, model); + const handleSelectChange = createSelectHandler(handleChange); + const handleTextChange = createTextHandler(handleChange); + + const htmlId = htmlIdGenerator(); + const indexPattern = + (props.series.override_index_pattern && props.series.series_index_pattern) || + props.panel.index_pattern; + + return ( + + + + + + + + + + + + } + fullWidth + > + + + + + + } + fullWidth + > + + + + + + ); +}; + +RateAgg.propTypes = { + disableDelete: PropTypes.bool, + fields: PropTypes.object, + model: PropTypes.object, + onAdd: PropTypes.func, + onChange: PropTypes.func, + onDelete: PropTypes.func, + panel: PropTypes.object, + series: PropTypes.object, + siblings: PropTypes.array, +}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js index ca40d60f208485..6bcb027e427f9c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js @@ -33,6 +33,7 @@ import { PercentileRankAgg } from '../aggs/percentile_rank'; import { Static } from '../aggs/static'; import { MathAgg } from '../aggs/math'; import { TopHitAgg } from '../aggs/top_hit'; +import { RateAgg } from '../aggs/rate'; export const aggToComponent = { count: StandardAgg, @@ -65,4 +66,5 @@ export const aggToComponent = { static: Static, math: MathAgg, top_hit: TopHitAgg, + rate: RateAgg, }; diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.js b/src/plugins/vis_type_timeseries/common/agg_lookup.js index 4dfdc83dcfabb8..1224cec7bee312 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.js @@ -97,6 +97,7 @@ export const lookup = { defaultMessage: 'Static Value', }), top_hit: i18n.translate('visTypeTimeseries.aggLookup.topHitLabel', { defaultMessage: 'Top Hit' }), + rate: i18n.translate('visTypeTimeseries.aggLookup.rateLabel', { defaultMessage: 'Rate' }), }; const pipeline = [ diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.js b/src/plugins/vis_type_timeseries/common/calculate_label.js index 756d6e57a83e8d..ca71df634427b4 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.js @@ -70,6 +70,11 @@ export function calculateLabel(metric, metrics) { defaultMessage: 'Filter Ratio', }); } + if (metric.type === 'rate') { + return i18n.translate('visTypeTimeseries.calculateLabel.rateLabel', { + defaultMessage: 'Rate', + }); + } if (metric.type === 'static') { return i18n.translate('visTypeTimeseries.calculateLabel.staticValueLabel', { defaultMessage: 'Static Value of {metricValue}', diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js index 4b0b8f33716a22..90f88e8de3a8c5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js @@ -26,6 +26,7 @@ import { dateHistogram } from './date_histogram'; import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; +import { rate } from './rate'; import { normalizeQuery } from './normalize_query'; export const processors = [ @@ -38,5 +39,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + rate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.js new file mode 100644 index 00000000000000..8dbff25a4933af --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.js @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { set } from 'lodash'; + +export const filter = metric => metric.type === 'rate'; + +export const createRate = (doc, intervalString, aggRoot) => metric => { + const maxFn = bucketTransform.max; + const derivativeFn = bucketTransform.derivative; + const positiveOnlyFn = bucketTransform.positive_only; + + const maxMetric = { id: `${metric.id}-rate-max`, type: 'max', field: metric.field }; + const derivativeMetric = { + id: `${metric.id}-rate-derivative`, + type: 'derivative', + field: `${metric.id}-rate-max`, + unit: metric.unit, + }; + const positiveOnlyMetric = { + id: metric.id, + type: 'positive_only', + field: `${metric.id}-rate-derivative`, + }; + + const fakeSeriesMetrics = [maxMetric, derivativeMetric, positiveOnlyMetric]; + + const maxBucket = maxFn(maxMetric, fakeSeriesMetrics, intervalString); + const derivativeBucket = derivativeFn(derivativeMetric, fakeSeriesMetrics, intervalString); + const positiveOnlyBucket = positiveOnlyFn(positiveOnlyMetric, fakeSeriesMetrics, intervalString); + + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-rate-max`, maxBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-rate-derivative`, derivativeBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, positiveOnlyBucket); +}; + +export function rate(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { intervalString } = getBucketSize(req, interval, capabilities); + if (series.metrics.some(filter)) { + series.metrics + .filter(filter) + .forEach(createRate(doc, intervalString, `aggs.${series.id}.aggs`)); + return next(doc); + } + return next(doc); + }; +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.test.js new file mode 100644 index 00000000000000..e09a699e7c14ab --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/rate.test.js @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { rate } from './rate'; +describe('rate(req, panel, series)', () => { + let panel; + let series; + let req; + beforeEach(() => { + panel = { + time_field: 'timestamp', + }; + series = { + id: 'test', + split_mode: 'terms', + terms_size: 10, + terms_field: 'host', + metrics: [ + { + id: 'metric-1', + type: 'rate', + field: 'system.network.out.bytes', + unit: '1s', + }, + ], + }; + req = { + payload: { + timerange: { + min: '2017-01-01T00:00:00Z', + max: '2017-01-01T01:00:00Z', + }, + }, + }; + }); + + test('calls next when finished', () => { + const next = jest.fn(); + rate(req, panel, series)(next)({}); + expect(next.mock.calls.length).toEqual(1); + }); + + test('returns rate aggs', () => { + const next = doc => doc; + const doc = rate(req, panel, series)(next)({}); + expect(doc).toEqual({ + aggs: { + test: { + aggs: { + timeseries: { + aggs: { + 'metric-1-rate-max': { + max: { field: 'system.network.out.bytes' }, + }, + 'metric-1-rate-derivative': { + derivative: { + buckets_path: 'metric-1-rate-max', + gap_policy: 'skip', + unit: '1s', + }, + }, + 'metric-1': { + bucket_script: { + buckets_path: { value: 'metric-1-rate-derivative[normalized_value]' }, + script: { + source: 'params.value > 0.0 ? params.value : 0.0', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js index a62533ae7a37ce..8fe79788958509 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js @@ -26,6 +26,7 @@ import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; import { normalizeQuery } from './normalize_query'; +import { rate } from './rate'; export const processors = [ query, @@ -36,5 +37,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + rate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/rate.js new file mode 100644 index 00000000000000..ffe81ac869ab2a --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/rate.js @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { calculateAggRoot } from './calculate_agg_root'; +import { createRate, filter } from '../series/rate'; + +export function rate(req, panel, esQueryConfig, indexPatternObject) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { intervalString } = getBucketSize(req, interval); + panel.series.forEach(column => { + const aggRoot = calculateAggRoot(doc, column); + column.metrics.filter(filter).forEach(createRate(doc, intervalString, aggRoot)); + }); + return next(doc); + }; +}