Skip to content

Commit

Permalink
[TSVB] Add Rate to Aggregations
Browse files Browse the repository at this point in the history
  • Loading branch information
simianhacker committed Mar 11, 2020
1 parent 4090149 commit 0ae5497
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ const metricAggs = [
}),
value: 'variance',
},
{
label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.rateLabel', {
defaultMessage: 'Rate',
}),
value: 'rate',
},
];

const pipelineAggs = [
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<AggRow
disableDelete={props.disableDelete}
model={props.model}
onAdd={props.onAdd}
onDelete={props.onDelete}
siblings={props.siblings}
dragHandleProps={props.dragHandleProps}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFormLabel htmlFor={htmlId('aggregation')}>
<FormattedMessage
id="visTypeTimeseries.derivative.aggregationLabel"
defaultMessage="Aggregation"
/>
</EuiFormLabel>
<EuiSpacer size="xs" />
<AggSelect
id={htmlId('aggregation')}
panelType={props.panel.type}
siblings={props.siblings}
value={model.type}
onChange={handleSelectChange('type')}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('field')}
label={
<FormattedMessage id="visTypeTimeseries.stdAgg.fieldLabel" defaultMessage="Field" />
}
fullWidth
>
<FieldSelect
fields={props.fields}
type={model.type}
restrict={KBN_FIELD_TYPES.NUMBER}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}
uiRestrictions={props.uiRestrictions}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('units')}
label={
<FormattedMessage
id="visTypeTimeseries.derivative.unitsLabel"
defaultMessage="Units (1s, 1m, etc)"
description="1s and 1m are required values and must not be translated."
/>
}
fullWidth
>
<EuiFieldText onChange={handleTextChange('unit')} value={model.unit} fullWidth />
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</AggRow>
);
};

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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -65,4 +66,5 @@ export const aggToComponent = {
static: Static,
math: MathAgg,
top_hit: TopHitAgg,
rate: RateAgg,
};
1 change: 1 addition & 0 deletions src/plugins/vis_type_timeseries/common/agg_lookup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/vis_type_timeseries/common/calculate_label.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -38,5 +39,6 @@ export const processors = [
metricBuckets,
siblingBuckets,
filterRatios,
rate,
normalizeQuery,
];
Original file line number Diff line number Diff line change
@@ -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);
};
}
Original file line number Diff line number Diff line change
@@ -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',
},
},
},
},
},
},
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,5 +37,6 @@ export const processors = [
metricBuckets,
siblingBuckets,
filterRatios,
rate,
normalizeQuery,
];
Loading

0 comments on commit 0ae5497

Please sign in to comment.