From 2f79b0607c55379d58815413e90b5dc13ee2bb1c Mon Sep 17 00:00:00 2001 From: Bill McConaghy Date: Fri, 26 Apr 2019 13:30:16 -0400 Subject: [PATCH] adding support for >=, <=, and between for threshold alerts (#35614) * adding support for >=, <=, and between for threshold alerts * adding i18n for AND label * making prettier happy * appeasing the linter * more linting issues * more linting * fixing test * fixing error state for threshold expression * fixing alignment for treshold inputs * addressing PR feedback * one more PR review fix * eslint fixes * fixing a couple of watch viz bugs --- .../watcher/common/constants/comparators.ts | 4 +- .../watcher/public/components/form_errors.tsx | 1 - .../public/models/watch/comparators.ts | 58 +++++++++++++ .../public/models/watch/threshold_watch.js | 31 +++++-- .../public/sections/watch_edit/comparators.ts | 28 ------ .../components/threshold_watch_edit.tsx | 86 +++++++++++++------ .../components/watch_visualization.tsx | 48 +++++++---- .../models/watch/__tests__/threshold_watch.js | 2 +- .../watch/threshold_watch/build_condition.js | 61 +++++++++++-- .../watch/threshold_watch/build_transform.js | 49 ++++++++++- .../watch/threshold_watch/threshold_watch.js | 2 +- 11 files changed, 279 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/watcher/public/models/watch/comparators.ts delete mode 100644 x-pack/plugins/watcher/public/sections/watch_edit/comparators.ts diff --git a/x-pack/plugins/watcher/common/constants/comparators.ts b/x-pack/plugins/watcher/common/constants/comparators.ts index 4a27b7e275b65e..21b350c0f8ce41 100644 --- a/x-pack/plugins/watcher/common/constants/comparators.ts +++ b/x-pack/plugins/watcher/common/constants/comparators.ts @@ -6,6 +6,8 @@ export const COMPARATORS: { [key: string]: string } = { GREATER_THAN: '>', - + GREATER_THAN_OR_EQUALS: '>=', + BETWEEN: 'between', LESS_THAN: '<', + LESS_THAN_OR_EQUALS: '<=', }; diff --git a/x-pack/plugins/watcher/public/components/form_errors.tsx b/x-pack/plugins/watcher/public/components/form_errors.tsx index 4314a8c877e259..0a35d6cb5d1046 100644 --- a/x-pack/plugins/watcher/public/components/form_errors.tsx +++ b/x-pack/plugins/watcher/public/components/form_errors.tsx @@ -6,7 +6,6 @@ import { EuiFormRow } from '@elastic/eui'; import React, { Children, cloneElement, Fragment, ReactElement } from 'react'; - export const ErrableFormRow = ({ errorKey, isShowingErrors, diff --git a/x-pack/plugins/watcher/public/models/watch/comparators.ts b/x-pack/plugins/watcher/public/models/watch/comparators.ts new file mode 100644 index 00000000000000..b636cdaf14c180 --- /dev/null +++ b/x-pack/plugins/watcher/public/models/watch/comparators.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { COMPARATORS } from '../../../common/constants'; + +export interface Comparator { + text: string; + value: string; + requiredValues: number; +} +export const comparators: { [key: string]: Comparator } = { + [COMPARATORS.GREATER_THAN]: { + text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isAboveLabel', { + defaultMessage: 'Is above', + }), + value: COMPARATORS.GREATER_THAN, + requiredValues: 1, + }, + [COMPARATORS.GREATER_THAN_OR_EQUALS]: { + text: i18n.translate( + 'xpack.watcher.thresholdWatchExpression.comparators.isAboveOrEqualsLabel', + { + defaultMessage: 'Is above or equals', + } + ), + value: COMPARATORS.GREATER_THAN_OR_EQUALS, + requiredValues: 1, + }, + [COMPARATORS.LESS_THAN]: { + text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isBelowLabel', { + defaultMessage: 'Is below', + }), + value: COMPARATORS.LESS_THAN, + requiredValues: 1, + }, + [COMPARATORS.LESS_THAN_OR_EQUALS]: { + text: i18n.translate( + 'xpack.watcher.thresholdWatchExpression.comparators.isBelowOrEqualsLabel', + { + defaultMessage: 'Is below or equals', + } + ), + value: COMPARATORS.LESS_THAN_OR_EQUALS, + requiredValues: 1, + }, + [COMPARATORS.BETWEEN]: { + text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isBetweenLabel', { + defaultMessage: 'Is between', + }), + value: COMPARATORS.BETWEEN, + requiredValues: 2, + }, +}; diff --git a/x-pack/plugins/watcher/public/models/watch/threshold_watch.js b/x-pack/plugins/watcher/public/models/watch/threshold_watch.js index ad6bf88294ab0f..fb27f657017084 100644 --- a/x-pack/plugins/watcher/public/models/watch/threshold_watch.js +++ b/x-pack/plugins/watcher/public/models/watch/threshold_watch.js @@ -11,6 +11,8 @@ import { getTimeUnitsLabel } from 'plugins/watcher/lib/get_time_units_label'; import { i18n } from '@kbn/i18n'; import { aggTypes } from './agg_types'; import { groupByTypes } from './group_by_types'; +import { comparators } from './comparators'; +const { BETWEEN } = COMPARATORS; const DEFAULT_VALUES = { AGG_TYPE: 'count', TERM_SIZE: 5, @@ -19,7 +21,7 @@ const DEFAULT_VALUES = { TIME_WINDOW_UNIT: 'm', TRIGGER_INTERVAL_SIZE: 1, TRIGGER_INTERVAL_UNIT: 'm', - THRESHOLD: 1000, + THRESHOLD: [1000, 5000], GROUP_BY: 'all', }; @@ -102,7 +104,6 @@ export class ThresholdWatch extends BaseWatch { aggField: [], termSize: [], termField: [], - threshold: [], timeWindowSize: [], }; validationResult.errors = errors; @@ -176,15 +177,29 @@ export class ThresholdWatch extends BaseWatch { ); } } - if (!this.threshold) { - errors.threshold.push( - i18n.translate( + + Array.from(Array(comparators[this.thresholdComparator].requiredValues)).forEach((value, i) => { + const key = `threshold${i}`; + errors[key] = []; + if (!this.threshold[i]) { + errors[key].push(i18n.translate( 'xpack.watcher.thresholdWatchExpression.thresholdLevel.valueIsRequiredValidationMessage', { defaultMessage: 'A value is required.', } - ) - ); + )); + } + }); + if (this.thresholdComparator === BETWEEN && this.threshold[0] && this.threshold[1] && !(this.threshold[1] > this.threshold[0])) { + errors.threshold1.push(i18n.translate( + 'xpack.watcher.thresholdWatchExpression.thresholdLevel.secondValueMustBeGreaterMessage', + { + defaultMessage: 'This value must be greater than {lowerBound}.', + values: { + lowerBound: this.threshold[0] + } + } + )); } if (!this.timeWindowSize) { errors.timeWindowSize.push( @@ -212,7 +227,7 @@ export class ThresholdWatch extends BaseWatch { thresholdComparator: this.thresholdComparator, timeWindowSize: this.timeWindowSize, timeWindowUnit: this.timeWindowUnit, - threshold: this.threshold, + threshold: comparators[this.thresholdComparator].requiredValues > 1 ? this.threshold : this.threshold[0], }); return result; diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/comparators.ts b/x-pack/plugins/watcher/public/sections/watch_edit/comparators.ts deleted file mode 100644 index 63508732f83891..00000000000000 --- a/x-pack/plugins/watcher/public/sections/watch_edit/comparators.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { COMPARATORS } from '../../../common/constants'; - -export interface Comparator { - text: string; - value: string; -} -export const comparators: { [key: string]: Comparator } = { - [COMPARATORS.GREATER_THAN]: { - text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isAboveLabel', { - defaultMessage: 'Is above', - }), - value: COMPARATORS.GREATER_THAN, - }, - [COMPARATORS.LESS_THAN]: { - text: i18n.translate('xpack.watcher.thresholdWatchExpression.comparators.isBelowLabel', { - defaultMessage: 'Is below', - }), - value: COMPARATORS.LESS_THAN, - }, -}; diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit.tsx index 14e10feb63573a..876c77d1f2b8f9 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit.tsx @@ -5,7 +5,6 @@ */ import React, { Fragment, useContext, useEffect, useState } from 'react'; - import { EuiButton, EuiButtonEmpty, @@ -32,7 +31,7 @@ import { ErrableFormRow } from '../../../components/form_errors'; import { fetchFields, getMatchingIndices, loadIndexPatterns } from '../../../lib/api'; import { aggTypes } from '../../../models/watch/agg_types'; import { groupByTypes } from '../../../models/watch/group_by_types'; -import { comparators } from '../comparators'; +import { comparators } from '../../../models/watch/comparators'; import { timeUnits } from '../time_units'; import { onWatchSave, saveWatch } from '../watch_edit_actions'; import { WatchContext } from './watch_context'; @@ -156,11 +155,21 @@ const ThresholdWatchEditUi = ({ intl, pageTitle }: { intl: InjectedIntl; pageTit defaultMessage: 'Please fix the errors in the expression below.', } ); - const expressionFields = ['aggField', 'termSize', 'termField', 'threshold', 'timeWindowSize']; + const expressionFields = [ + 'aggField', + 'termSize', + 'termField', + 'threshold0', + 'threshold1', + 'timeWindowSize', + ]; const hasExpressionErrors = !!Object.keys(errors).find( errorKey => expressionFields.includes(errorKey) && errors[errorKey].length >= 1 ); const shouldShowThresholdExpression = watch.index && watch.index.length > 0 && watch.timeField; + const andThresholdText = i18n.translate('xpack.watcher.sections.watchEdit.threshold.andLabel', { + defaultMessage: 'AND', + }); return ( @@ -630,12 +639,22 @@ const ThresholdWatchEditUi = ({ intl, pageTitle }: { intl: InjectedIntl; pageTit button={ { setWatchThresholdPopoverOpen(true); }} - color={watch.threshold ? 'secondary' : 'danger'} + color={ + errors.threshold0.length || (errors.threshold1 && errors.threshold1.length) + ? 'danger' + : 'secondary' + } /> } isOpen={watchThresholdPopoverOpen} @@ -648,33 +667,50 @@ const ThresholdWatchEditUi = ({ intl, pageTitle }: { intl: InjectedIntl; pageTit >
{comparators[watch.thresholdComparator].text} - + { setWatchProperty('thresholdComparator', e.target.value); }} - options={Object.values(comparators)} + options={Object.values(comparators).map(({ text, value }) => { + return { text, value }; + })} /> - - - { - const { value } = e.target; - const threshold = value !== '' ? parseInt(value, 10) : value; - setWatchProperty('threshold', threshold); - }} - /> - - + {Array.from(Array(comparators[watch.thresholdComparator].requiredValues)).map( + (notUsed, i) => { + return ( + + {i > 0 ? ( + + {andThresholdText} + + ) : null} + + + { + const { value } = e.target; + const threshold = value !== '' ? parseInt(value, 10) : value; + const newThreshold = [...watch.threshold]; + newThreshold[i] = threshold; + setWatchProperty('threshold', newThreshold); + }} + /> + + + + ); + } + )}
diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_visualization.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_visualization.tsx index 890e284fea6a8c..2773a9677683e5 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_visualization.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_visualization.tsx @@ -31,6 +31,7 @@ import { VisualizeOptions } from 'plugins/watcher/models/visualize_options'; import { getWatchVisualizationData } from '../../../lib/api'; import { WatchContext } from './watch_context'; import { aggTypes } from '../../../models/watch/agg_types'; +import { comparators } from '../../../models/watch/comparators'; const getChartTheme = () => { const isDarkTheme = chrome.getUiSettingsClient().get('theme:darkMode'); const baseTheme = isDarkTheme ? DARK_THEME : LIGHT_THEME; @@ -82,7 +83,9 @@ const getDomain = (watch: any) => { max: visualizeTimeWindowTo, }; }; - +const getThreshold = (watch: any) => { + return watch.threshold.slice(0, comparators[watch.thresholdComparator].requiredValues); +}; const getTimeBuckets = (watch: any) => { const domain = getDomain(watch); const timeBuckets = new TimeBuckets(); @@ -123,12 +126,17 @@ const WatchVisualizationUi = () => { .format(getTimeBuckets(watch).getScaledDateFormat()); }; const aggLabel = aggTypes[watch.aggType].text; - const thresholdCustomSeriesColors: CustomSeriesColorsMap = new Map(); - const thresholdDataSeriesColorValues: DataSeriesColorsValues = { - colorValues: [], - specId: getSpecId('threshold'), + + const getCustomColors = (specId: string) => { + const customSeriesColors: CustomSeriesColorsMap = new Map(); + const dataSeriesColorValues: DataSeriesColorsValues = { + colorValues: [], + specId: getSpecId(specId), + }; + customSeriesColors.set(dataSeriesColorValues, '#BD271E'); + return customSeriesColors; }; - thresholdCustomSeriesColors.set(thresholdDataSeriesColorValues, '#BD271E'); + const domain = getDomain(watch); const watchVisualizationDataKeys = Object.keys(watchVisualizationData); return ( @@ -163,17 +171,23 @@ const WatchVisualizationUi = () => { /> ); })} - + {getThreshold(watch).map((value: any, i: number) => { + const specId = i === 0 ? 'threshold' : `threshold${i}`; + return ( + + ); + })} ) : ( { thresholdComparator: 'thresholdComparator', timeWindowSize: 'timeWindowSize', timeWindowUnit: 'timeWindowUnit', - threshold: 'threshold' + threshold: ['threshold'] }; expect(actual).to.eql(expected); diff --git a/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_condition.js b/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_condition.js index 1c8d2ad422edb7..64ee0f44c31042 100644 --- a/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_condition.js +++ b/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_condition.js @@ -5,7 +5,8 @@ */ import { singleLineScript } from '../lib/single_line_script'; - +import { COMPARATORS } from '../../../../common/constants'; +const { BETWEEN } = COMPARATORS; /* watch.condition.script.inline */ @@ -13,17 +14,39 @@ function buildInline({ aggType, thresholdComparator, hasTermsAgg }) { let script = ''; if (aggType === 'count' && !hasTermsAgg) { - script = ` + if (thresholdComparator === BETWEEN) { + script = ` + if (ctx.payload.hits.total >= params.threshold[0] && ctx.payload.hits.total <= params.threshold[1]) { + return true; + } + + return false; + `; + } else { + script = ` if (ctx.payload.hits.total ${thresholdComparator} params.threshold) { return true; } return false; `; + } } if (aggType === 'count' && hasTermsAgg) { - script = ` + if (thresholdComparator === BETWEEN) { + script = ` + ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; + for (int i = 0; i < arr.length; i++) { + if (arr[i].doc_count >= params.threshold[0] && arr[i].doc_count <= params.threshold[1]) { + return true; + } + } + + return false; + `; + } else { + script = ` ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; for (int i = 0; i < arr.length; i++) { if (arr[i].doc_count ${thresholdComparator} params.threshold) { @@ -33,20 +56,44 @@ function buildInline({ aggType, thresholdComparator, hasTermsAgg }) { return false; `; - } + } + } if (aggType !== 'count' && !hasTermsAgg) { - script = ` + if (thresholdComparator === BETWEEN) { + script = ` + if (ctx.payload.aggregations.metricAgg.value >= params.threshold[0] + && ctx.payload.aggregations.metricAgg.value <= params.threshold[1]) { + return true; + } + + return false; + `; + } else { + script = ` if (ctx.payload.aggregations.metricAgg.value ${thresholdComparator} params.threshold) { return true; } return false; `; + } } if (aggType !== 'count' && hasTermsAgg) { - script = ` + if (thresholdComparator === BETWEEN) { + script = ` + ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; + for (int i = 0; i < arr.length; i++) { + if (arr[i]['metricAgg'].value >= params.threshold[0] && arr[i]['metricAgg'].value <= params.threshold[1]) { + return true; + } + } + + return false; + `; + } else { + script = ` ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; for (int i = 0; i < arr.length; i++) { if (arr[i]['metricAgg'].value ${thresholdComparator} params.threshold) { @@ -56,6 +103,8 @@ function buildInline({ aggType, thresholdComparator, hasTermsAgg }) { return false; `; + } + } return singleLineScript(script); diff --git a/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_transform.js b/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_transform.js index d2a6116ca803f7..a302dde245634a 100644 --- a/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_transform.js +++ b/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_transform.js @@ -5,7 +5,8 @@ */ import { singleLineScript } from '../lib/single_line_script'; - +import { COMPARATORS } from '../../../../common/constants'; +const { BETWEEN } = COMPARATORS; /* watch.transform.script.inline */ @@ -22,7 +23,26 @@ function buildInline({ aggType, hasTermsAgg, thresholdComparator }) { } if (aggType === 'count' && hasTermsAgg) { - script = ` + if (thresholdComparator === BETWEEN) { + script = ` + HashMap result = new HashMap(); + ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; + ArrayList filteredHits = new ArrayList(); + + for (int i = 0; i < arr.length; i++) { + HashMap filteredHit = new HashMap(); + filteredHit.key = arr[i].key; + filteredHit.value = arr[i].doc_count; + if (filteredHit.value >= params.threshold[0] && filteredHit.value <= params.threshold[1]) { + filteredHits.add(filteredHit); + } + } + result.results = filteredHits; + + return result; + `; + } else { + script = ` HashMap result = new HashMap(); ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; ArrayList filteredHits = new ArrayList(); @@ -39,6 +59,8 @@ function buildInline({ aggType, hasTermsAgg, thresholdComparator }) { return result; `; + } + } if (aggType !== 'count' && !hasTermsAgg) { @@ -51,7 +73,26 @@ function buildInline({ aggType, hasTermsAgg, thresholdComparator }) { } if (aggType !== 'count' && hasTermsAgg) { - script = ` + if (thresholdComparator === BETWEEN) { + script = ` + HashMap result = new HashMap(); + ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; + ArrayList filteredHits = new ArrayList(); + + for (int i = 0; i < arr.length; i++) { + HashMap filteredHit = new HashMap(); + filteredHit.key = arr[i].key; + filteredHit.value = arr[i]['metricAgg'].value; + if (filteredHit.value >= params.threshold[0] && filteredHit.value <= params.threshold[1]) { + filteredHits.add(filteredHit); + } + } + result.results = filteredHits; + + return result; + `; + } else { + script = ` HashMap result = new HashMap(); ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; ArrayList filteredHits = new ArrayList(); @@ -68,6 +109,8 @@ function buildInline({ aggType, hasTermsAgg, thresholdComparator }) { return result; `; + } + } return singleLineScript(script); diff --git a/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js b/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js index eaf9feabf7bf97..4fbc0111f57dd2 100644 --- a/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js +++ b/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js @@ -120,7 +120,7 @@ export class ThresholdWatch extends BaseWatch { thresholdComparator: metadata.threshold_comparator, timeWindowSize: metadata.time_window_size, timeWindowUnit: metadata.time_window_unit, - threshold: metadata.threshold + threshold: Array.isArray(metadata.threshold) ? metadata.threshold : [metadata.threshold] } );