[Alerting] Configurable number of hits for ES query alert (#90089)

* Adding size parameter to ES query alert

* Can't use const inside validation

* Updating docs

* Fixing functional test

* License

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
ymao1 2021-02-09 14:07:53 -05:00 committed by GitHub
parent 1f5d52ea2e
commit 5f8de693b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 382 additions and 8 deletions

View file

@ -130,12 +130,13 @@ image::images/alert-types-es-query-select.png[Choosing an ES query alert type]
[float]
==== Defining the conditions
The ES query alert has 4 clauses that define the condition to detect.
The ES query alert has 5 clauses that define the condition to detect.
[role="screenshot"]
image::images/alert-types-es-query-conditions.png[Four clauses define the condition to detect]
Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*.
Size:: This clause specifies the number of documents to pass to the configured actions when the the threshold condition is met.
ES query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold
condition. Aggregations are not supported at this time.
Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View file

@ -126,6 +126,7 @@ describe('EsQueryAlertTypeExpression', () => {
index: ['test-index'],
timeField: '@timestamp',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
thresholdComparator: '>',
threshold: [0],
timeWindowSize: 15,
@ -137,6 +138,7 @@ describe('EsQueryAlertTypeExpression', () => {
const errors = {
index: [],
esQuery: [],
size: [],
timeField: [],
timeWindowSize: [],
};
@ -169,6 +171,7 @@ describe('EsQueryAlertTypeExpression', () => {
test('should render EsQueryAlertTypeExpression with expected components', async () => {
const wrapper = await setup(getAlertParams());
expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="queryJsonEditor"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy();

View file

@ -30,6 +30,7 @@ import {
COMPARATORS,
ThresholdExpression,
ForLastExpression,
ValueExpression,
AlertTypeParamsExpressionProps,
} from '../../../../triggers_actions_ui/public';
import { validateExpression } from './validation';
@ -45,6 +46,7 @@ const DEFAULT_VALUES = {
"match_all" : {}
}
}`,
SIZE: 100,
TIME_WINDOW_SIZE: 5,
TIME_WINDOW_UNIT: 'm',
THRESHOLD: [1000],
@ -53,6 +55,7 @@ const DEFAULT_VALUES = {
const expressionFieldsWithValidation = [
'index',
'esQuery',
'size',
'timeField',
'threshold0',
'threshold1',
@ -74,6 +77,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
index,
timeField,
esQuery,
size,
thresholdComparator,
threshold,
timeWindowSize,
@ -83,6 +87,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
const getDefaultParams = () => ({
...alertParams,
esQuery: esQuery ?? DEFAULT_VALUES.QUERY,
size: size ?? DEFAULT_VALUES.SIZE,
timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
@ -214,7 +219,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
<h5>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.selectIndex"
defaultMessage="Select an index"
defaultMessage="Select an index and size"
/>
</h5>
</EuiTitle>
@ -234,6 +239,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
...alertParams,
index: indices,
esQuery: DEFAULT_VALUES.QUERY,
size: DEFAULT_VALUES.SIZE,
thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR,
timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT,
@ -246,6 +252,19 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
}}
onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)}
/>
<ValueExpression
description={i18n.translate('xpack.stackAlerts.esQuery.ui.sizeExpression', {
defaultMessage: 'Size',
})}
data-test-subj="sizeValueExpression"
value={size}
errors={errors.size}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedValue={(updatedValue) => {
setParam('size', updatedValue);
}}
/>
<EuiSpacer />
<EuiTitle size="xs">
<h5>

View file

@ -17,6 +17,7 @@ export interface EsQueryAlertParams extends AlertTypeParams {
index: string[];
timeField?: string;
esQuery: string;
size: number;
thresholdComparator?: string;
threshold: number[];
timeWindowSize: number;

View file

@ -13,6 +13,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: [],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@ -25,6 +26,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@ -37,6 +39,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@ -49,6 +52,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"aggs\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@ -61,6 +65,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
threshold: [],
timeWindowSize: 1,
timeWindowUnit: 's',
@ -74,6 +79,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
threshold: [1],
timeWindowSize: 1,
timeWindowUnit: 's',
@ -87,6 +93,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
threshold: [10, 1],
timeWindowSize: 1,
timeWindowUnit: 's',
@ -97,4 +104,34 @@ describe('expression params validation', () => {
'Threshold 1 must be > Threshold 0.'
);
});
test('if size property is < 0 should return proper error message', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
size: -1,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
};
expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.size[0]).toBe(
'Size must be between 0 and 10,000.'
);
});
test('if size property is > 10000 should return proper error message', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
size: 25000,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
};
expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.size[0]).toBe(
'Size must be between 0 and 10,000.'
);
});
});

View file

@ -10,12 +10,21 @@ import { EsQueryAlertParams } from './types';
import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public';
export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => {
const { index, timeField, esQuery, threshold, timeWindowSize, thresholdComparator } = alertParams;
const {
index,
timeField,
esQuery,
size,
threshold,
timeWindowSize,
thresholdComparator,
} = alertParams;
const validationResult = { errors: {} };
const errors = {
index: new Array<string>(),
timeField: new Array<string>(),
esQuery: new Array<string>(),
size: new Array<string>(),
threshold0: new Array<string>(),
threshold1: new Array<string>(),
thresholdComparator: new Array<string>(),
@ -94,5 +103,20 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
})
);
}
if (!size) {
errors.size.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', {
defaultMessage: 'Size is required.',
})
);
}
if ((size && size < 0) || size > 10000) {
errors.size.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.invalidSizeRangeText', {
defaultMessage: 'Size must be between 0 and {max, number}.',
values: { max: 10000 },
})
);
}
return validationResult;
};

View file

@ -14,6 +14,7 @@ describe('ActionContext', () => {
index: ['[index]'],
timeField: '[timeField]',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: '>',
@ -41,6 +42,7 @@ describe('ActionContext', () => {
index: ['[index]'],
timeField: '[timeField]',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: 'between',

View file

@ -57,6 +57,10 @@ describe('alertType', () => {
"description": "The string representation of the ES query.",
"name": "esQuery",
},
Object {
"description": "The number of hits to retrieve for each query.",
"name": "size",
},
Object {
"description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.",
"name": "threshold",
@ -75,6 +79,7 @@ describe('alertType', () => {
index: ['index-name'],
timeField: 'time-field',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: '<',
@ -92,6 +97,7 @@ describe('alertType', () => {
index: ['index-name'],
timeField: 'time-field',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: 'between',

View file

@ -23,8 +23,6 @@ import { ESSearchHit } from '../../../../../typings/elasticsearch';
export const ES_QUERY_ID = '.es-query';
const DEFAULT_MAX_HITS_PER_EXECUTION = 1000;
const ActionGroupId = 'query matched';
const ConditionMetAlertInstanceId = 'query matched';
@ -88,6 +86,13 @@ export function getAlertType(
}
);
const actionVariableContextSizeLabel = i18n.translate(
'xpack.stackAlerts.esQuery.actionVariableContextSizeLabel',
{
defaultMessage: 'The number of hits to retrieve for each query.',
}
);
const actionVariableContextThresholdLabel = i18n.translate(
'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel',
{
@ -130,6 +135,7 @@ export function getAlertType(
params: [
{ name: 'index', description: actionVariableContextIndexLabel },
{ name: 'esQuery', description: actionVariableContextQueryLabel },
{ name: 'size', description: actionVariableContextSizeLabel },
{ name: 'threshold', description: actionVariableContextThresholdLabel },
{ name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel },
],
@ -160,7 +166,7 @@ export function getAlertType(
}
// During each alert execution, we run the configured query, get a hit count
// (hits.total) and retrieve up to DEFAULT_MAX_HITS_PER_EXECUTION hits. We
// (hits.total) and retrieve up to params.size hits. We
// evaluate the threshold condition using the value of hits.total. If the threshold
// condition is met, the hits are counted toward the query match and we update
// the alert state with the timestamp of the latest hit. In the next execution
@ -200,7 +206,7 @@ export function getAlertType(
from: dateStart,
to: dateEnd,
filter,
size: DEFAULT_MAX_HITS_PER_EXECUTION,
size: params.size,
sortOrder: 'desc',
searchAfterSortId: undefined,
timeField: params.timeField,

View file

@ -7,12 +7,17 @@
import { TypeOf } from '@kbn/config-schema';
import type { Writable } from '@kbn/utility-types';
import { EsQueryAlertParamsSchema, EsQueryAlertParams } from './alert_type_params';
import {
EsQueryAlertParamsSchema,
EsQueryAlertParams,
ES_QUERY_MAX_HITS_PER_EXECUTION,
} from './alert_type_params';
const DefaultParams: Writable<Partial<EsQueryAlertParams>> = {
index: ['index-name'],
timeField: 'time-field',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: '>',
@ -99,6 +104,28 @@ describe('alertType Params validate()', () => {
);
});
it('fails for invalid size', async () => {
delete params.size;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[size]: expected value of type [number] but got [undefined]"`
);
params.size = 'foo';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[size]: expected value of type [number] but got [string]"`
);
params.size = -1;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[size]: Value must be equal to or greater than [0]."`
);
params.size = ES_QUERY_MAX_HITS_PER_EXECUTION + 1;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[size]: Value must be equal to or lower than [10000]."`
);
});
it('fails for invalid timeWindowSize', async () => {
delete params.timeWindowSize;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(

View file

@ -11,6 +11,8 @@ import { ComparatorFnNames } from '../lib';
import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server';
import { AlertTypeState } from '../../../../alerts/server';
export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000;
// alert type parameters
export type EsQueryAlertParams = TypeOf<typeof EsQueryAlertParamsSchema>;
export interface EsQueryAlertState extends AlertTypeState {
@ -21,6 +23,7 @@ export const EsQueryAlertParamsSchemaProperties = {
index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
timeField: schema.string({ minLength: 1 }),
esQuery: schema.string({ minLength: 1 }),
size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }),
timeWindowSize: schema.number({ min: 1 }),
timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }),
threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }),

View file

@ -10,3 +10,4 @@ export { OfExpression } from './of';
export { GroupByExpression } from './group_by_over';
export { ThresholdExpression } from './threshold';
export { ForLastExpression } from './for_the_last';
export { ValueExpression } from './value';

View file

@ -0,0 +1,136 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { ValueExpression } from './value';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
describe('value expression', () => {
it('renders description and value', () => {
const wrapper = shallow(
<ValueExpression
description="test"
value={1000}
errors={[]}
onChangeSelectedValue={jest.fn()}
/>
);
expect(wrapper.find('[data-test-subj="valueFieldTitle"]')).toMatchInlineSnapshot(`
<ClosablePopoverTitle
data-test-subj="valueFieldTitle"
onClose={[Function]}
>
test
</ClosablePopoverTitle>
`);
expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(`
<EuiFormRow
data-test-subj="valueFieldNumberForm"
describedByIds={Array []}
display="row"
error={Array []}
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
isInvalid={false}
labelType="label"
>
<EuiFieldNumber
data-test-subj="valueFieldNumber"
isInvalid={false}
min={0}
onChange={[Function]}
value={1000}
/>
</EuiFormRow>
`);
});
it('renders errors', () => {
const wrapper = shallow(
<ValueExpression
description="test"
value={1000}
errors={['value is not valid']}
onChangeSelectedValue={jest.fn()}
/>
);
expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(`
<EuiFormRow
data-test-subj="valueFieldNumberForm"
describedByIds={Array []}
display="row"
error={
Array [
"value is not valid",
]
}
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
isInvalid={true}
labelType="label"
>
<EuiFieldNumber
data-test-subj="valueFieldNumber"
isInvalid={true}
min={0}
onChange={[Function]}
value={1000}
/>
</EuiFormRow>
`);
});
it('renders closed popover initially and opens on click', async () => {
const wrapper = mountWithIntl(
<ValueExpression
description="test"
value={1000}
errors={[]}
onChangeSelectedValue={jest.fn()}
/>
);
expect(wrapper.find('[data-test-subj="valueExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeFalsy();
wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeTruthy();
});
it('emits onChangeSelectedValue action when value is updated', async () => {
const onChangeSelectedValue = jest.fn();
const wrapper = mountWithIntl(
<ValueExpression
description="test"
value={1000}
errors={[]}
onChangeSelectedValue={onChangeSelectedValue}
/>
);
wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
wrapper
.find('input[data-test-subj="valueFieldNumber"]')
.simulate('change', { target: { value: 3000 } });
expect(onChangeSelectedValue).toHaveBeenCalledWith(3000);
});
});

View file

@ -0,0 +1,102 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import {
EuiExpression,
EuiPopover,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
} from '@elastic/eui';
import { ClosablePopoverTitle } from './components';
import { IErrorObject } from '../../types';
interface ValueExpressionProps {
description: string;
value: number;
onChangeSelectedValue: (updatedValue: number) => void;
popupPosition?:
| 'upCenter'
| 'upLeft'
| 'upRight'
| 'downCenter'
| 'downLeft'
| 'downRight'
| 'leftCenter'
| 'leftUp'
| 'leftDown'
| 'rightCenter'
| 'rightUp'
| 'rightDown';
display?: 'fullWidth' | 'inline';
errors: string | string[] | IErrorObject;
}
export const ValueExpression = ({
description,
value,
onChangeSelectedValue,
display = 'inline',
popupPosition,
errors,
}: ValueExpressionProps) => {
const [valuePopoverOpen, setValuePopoverOpen] = useState(false);
return (
<EuiPopover
button={
<EuiExpression
data-test-subj="valueExpression"
description={description}
value={value}
isActive={valuePopoverOpen}
display={display === 'inline' ? 'inline' : 'columns'}
onClick={() => {
setValuePopoverOpen(true);
}}
/>
}
isOpen={valuePopoverOpen}
closePopover={() => {
setValuePopoverOpen(false);
}}
ownFocus
display={display === 'fullWidth' ? 'block' : 'inlineBlock'}
anchorPosition={popupPosition ?? 'downLeft'}
repositionOnScroll
>
<div>
<ClosablePopoverTitle
data-test-subj="valueFieldTitle"
onClose={() => setValuePopoverOpen(false)}
>
<>{description}</>
</ClosablePopoverTitle>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiFormRow
data-test-subj="valueFieldNumberForm"
isInvalid={errors.length > 0 && value !== undefined}
error={errors}
>
<EuiFieldNumber
data-test-subj="valueFieldNumber"
min={0}
value={value}
isInvalid={errors.length > 0 && value !== undefined}
onChange={(e: any) => {
onChangeSelectedValue(e.target.value as number);
}}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</EuiPopover>
);
};

View file

@ -68,6 +68,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await createAlert({
name: 'never fire',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
thresholdComparator: '<',
threshold: [0],
});
@ -75,6 +76,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await createAlert({
name: 'always fire',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
thresholdComparator: '>',
threshold: [-1],
});
@ -123,6 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await createAlert({
name: 'never fire',
esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)),
size: 100,
thresholdComparator: '>=',
threshold: [0],
});
@ -132,6 +135,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
esQuery: JSON.stringify(
rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2))
),
size: 100,
thresholdComparator: '>=',
threshold: [0],
});
@ -173,6 +177,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
name: string;
timeField?: string;
esQuery: string;
size: number;
thresholdComparator: string;
threshold: number[];
timeWindowSize?: number;
@ -215,6 +220,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
index: [ES_TEST_INDEX_NAME],
timeField: params.timeField || 'date',
esQuery: params.esQuery,
size: params.size,
timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5,
timeWindowUnit: 's',
thresholdComparator: params.thresholdComparator,