From 0a23bfb08cb695f93a62854806bea604bfe4e321 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 3 Jun 2020 13:29:54 +0200 Subject: [PATCH] TSVB: handle division by zero in math agg (#67111) --- .../response_processors/series/math.js | 36 +++-- .../response_processors/series/math.test.js | 148 ++++++++++++++++++ 2 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js index 6c8fe7ca16619..f8752ce8fa3a8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js @@ -91,21 +91,29 @@ export function mathAgg(resp, panel, series, meta) { // a safety check for the user const someNull = values(params).some((v) => v == null); if (someNull) return [ts, null]; - // calculate the result based on the user's script and return the value - const result = evaluate(mathMetric.script, { - params: { - ...params, - _index: index, - _timestamp: ts, - _all: all, - _interval: split.meta.bucketSize * 1000, - }, - }); - // if the result is an object (usually when the user is working with maps and functions) flatten the results and return the last value. - if (typeof result === 'object') { - return [ts, last(flatten(result.valueOf()))]; + try { + // calculate the result based on the user's script and return the value + const result = evaluate(mathMetric.script, { + params: { + ...params, + _index: index, + _timestamp: ts, + _all: all, + _interval: split.meta.bucketSize * 1000, + }, + }); + // if the result is an object (usually when the user is working with maps and functions) flatten the results and return the last value. + if (typeof result === 'object') { + return [ts, last(flatten(result.valueOf()))]; + } + return [ts, result]; + } catch (e) { + if (e.message === 'Cannot divide by 0') { + // Drop division by zero errors and treat as null value + return [ts, null]; + } + throw e; } - return [ts, result]; }); return { id: split.id, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js new file mode 100644 index 0000000000000..79cfd2ddd54bb --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js @@ -0,0 +1,148 @@ +/* + * 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 { mathAgg } from './math'; +import { stdMetric } from './std_metric'; + +describe('math(resp, panel, series)', () => { + let panel; + let series; + let resp; + beforeEach(() => { + panel = { + time_field: 'timestamp', + }; + series = { + chart_type: 'line', + stacked: false, + line_width: 1, + point_size: 1, + fill: 0, + id: 'test', + label: 'Math', + split_mode: 'terms', + split_color_mode: 'gradient', + color: '#F00', + metrics: [ + { + id: 'avgcpu', + type: 'avg', + field: 'cpu', + }, + { + id: 'mincpu', + type: 'min', + field: 'cpu', + }, + { + id: 'mathagg', + type: 'math', + script: 'divide(params.a, params.b)', + variables: [ + { name: 'a', field: 'avgcpu' }, + { name: 'b', field: 'mincpu' }, + ], + }, + ], + }; + resp = { + aggregations: { + test: { + meta: { + bucketSize: 5, + }, + buckets: [ + { + key: 'example-01', + timeseries: { + buckets: [ + { + key: 1, + avgcpu: { value: 0.25 }, + mincpu: { value: 0.125 }, + }, + { + key: 2, + avgcpu: { value: 0.25 }, + mincpu: { value: 0.25 }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + test('calls next when finished', () => { + const next = jest.fn(); + mathAgg(resp, panel, series)(next)([]); + expect(next.mock.calls.length).toEqual(1); + }); + + test('creates a series', () => { + const next = mathAgg(resp, panel, series)((results) => results); + const results = stdMetric(resp, panel, series)(next)([]); + expect(results).toHaveLength(1); + + expect(results[0]).toEqual({ + id: 'test:example-01', + label: 'example-01', + color: 'rgb(255, 0, 0)', + stack: false, + seriesId: 'test', + lines: { show: true, fill: 0, lineWidth: 1, steps: false }, + points: { show: true, radius: 1, lineWidth: 1 }, + bars: { fill: 0, lineWidth: 1, show: false }, + data: [ + [1, 2], + [2, 1], + ], + }); + }); + + test('turns division by zero into null values', () => { + resp.aggregations.test.buckets[0].timeseries.buckets[0].mincpu = 0; + const next = mathAgg(resp, panel, series)((results) => results); + const results = stdMetric(resp, panel, series)(next)([]); + expect(results).toHaveLength(1); + + expect(results[0]).toEqual( + expect.objectContaining({ + data: [ + [1, null], + [2, 1], + ], + }) + ); + }); + + test('throws on actual tinymath expression errors', () => { + series.metrics[2].script = 'notExistingFn(params.a)'; + expect(() => + stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([]) + ).toThrow(); + + series.metrics[2].script = 'divide(params.a, params.b'; + expect(() => + stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([]) + ).toThrow(); + }); +});