Add "exclude previous hits" check box to ESQuery rule form (#138781)

* Add "Exclude the hits from previous rule run" check box to ESQuery rule form
This commit is contained in:
Ersin Erdal 2022-09-05 13:09:09 +02:00 committed by GitHub
parent 01ecbd48f3
commit 7d3f762186
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 280 additions and 42 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Before After
Before After

View file

@ -20,7 +20,7 @@ Fill in the <<defining-rules-general-details, rule details>>, then select
Define properties to detect the condition.
[role="screenshot"]
image::user/alerting/images/rule-types-es-query-conditions.png[Five clauses define the condition to detect]
image::user/alerting/images/rule-types-es-query-conditions.png[Six clauses define the condition to detect]
Index:: Specifies an *index or data view* and a *time field* that is used for
the *time window*.
@ -37,8 +37,9 @@ Time window:: Defines how far back to search for documents, using the
*time field* set in the *index* clause. Generally this value should be set to a
value higher than the *check every* value in the
<<defining-rules-general-details, general rule details>>, to avoid gaps in
detection.
detection.
Exclude the hits from previous run:: Turn on to avoid alert duplication by
excluding documents that have already been detected by the previous rule run.
[float]
==== Add action variables

View file

@ -19,6 +19,7 @@ export const DEFAULT_VALUES = {
TIME_WINDOW_SIZE: 5,
TIME_WINDOW_UNIT: 'm',
THRESHOLD: [1000],
EXCLUDE_PREVIOUS_HITS: true,
};
export const EXPRESSION_ERRORS = {

View file

@ -115,6 +115,7 @@ const defaultEsQueryExpressionParams: EsQueryAlertParams<SearchType.esQuery> = {
index: ['test-index'],
timeField: '@timestamp',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
excludeHitsFromPreviousRun: true,
};
describe('EsQueryAlertTypeExpression', () => {
@ -181,6 +182,12 @@ describe('EsQueryAlertTypeExpression', () => {
expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy();
const excludeHitsButton = wrapper.find(
'[data-test-subj="excludeHitsFromPreviousRunExpression"]'
);
expect(excludeHitsButton.exists()).toBeTruthy();
expect(excludeHitsButton.first().prop('checked')).toBeTruthy();
const testQueryButton = wrapper.find('EuiButton[data-test-subj="testQuery"]');
expect(testQueryButton.exists()).toBeTruthy();
expect(testQueryButton.prop('disabled')).toBe(false);

View file

@ -44,6 +44,7 @@ export const EsQueryExpression: React.FC<
threshold,
timeWindowSize,
timeWindowUnit,
excludeHitsFromPreviousRun,
} = ruleParams;
const [currentRuleParams, setCurrentRuleParams] = useState<
@ -57,6 +58,7 @@ export const EsQueryExpression: React.FC<
size: size ?? DEFAULT_VALUES.SIZE,
esQuery: esQuery ?? DEFAULT_VALUES.QUERY,
searchType: SearchType.esQuery,
excludeHitsFromPreviousRun: excludeHitsFromPreviousRun ?? DEFAULT_VALUES.EXCLUDE_PREVIOUS_HITS,
});
const setParam = useCallback(
@ -251,6 +253,10 @@ export const EsQueryExpression: React.FC<
errors={errors}
hasValidationErrors={hasValidationErrors()}
onTestFetch={onTestQuery}
excludeHitsFromPreviousRun={excludeHitsFromPreviousRun}
onChangeExcludeHitsFromPreviousRun={(exclude) => {
setParam('excludeHitsFromPreviousRun', exclude);
}}
/>
<EuiSpacer />

View file

@ -52,6 +52,7 @@ const defaultEsQueryRuleParams: EsQueryAlertParams<SearchType.esQuery> = {
timeField: '@timestamp',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
searchType: SearchType.esQuery,
excludeHitsFromPreviousRun: true,
};
const defaultSearchSourceRuleParams: EsQueryAlertParams<SearchType.searchSource> = {
size: 100,
@ -63,6 +64,7 @@ const defaultSearchSourceRuleParams: EsQueryAlertParams<SearchType.searchSource>
timeField: '@timestamp',
searchType: SearchType.searchSource,
searchConfiguration: {},
excludeHitsFromPreviousRun: true,
};
const dataViewPluginMock = dataViewPluginMocks.createStartContract();

View file

@ -54,6 +54,7 @@ const defaultSearchSourceExpressionParams: EsQueryAlertParams<SearchType.searchS
},
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
},
excludeHitsFromPreviousRun: true,
};
const mockSearchResult = new Subject();
@ -230,6 +231,10 @@ describe('SearchSourceAlertTypeExpression', () => {
});
wrapper = await wrapper.update();
expect(findTestSubject(wrapper, 'thresholdExpression')).toBeTruthy();
const excludeHitsCheckbox = findTestSubject(wrapper, 'excludeHitsFromPreviousRunExpression');
expect(excludeHitsCheckbox).toBeTruthy();
expect(excludeHitsCheckbox.prop('checked')).toBeTruthy();
});
test('should disable Test Query button if data view is not selected yet', async () => {

View file

@ -34,6 +34,7 @@ export const SearchSourceExpression = ({
size,
savedQueryId,
searchConfiguration,
excludeHitsFromPreviousRun,
} = ruleParams;
const { data } = useTriggersAndActionsUiDeps();
@ -69,6 +70,8 @@ export const SearchSourceExpression = ({
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
size: size ?? DEFAULT_VALUES.SIZE,
excludeHitsFromPreviousRun:
excludeHitsFromPreviousRun ?? DEFAULT_VALUES.EXCLUDE_PREVIOUS_HITS,
});
data.search.searchSource

View file

@ -38,13 +38,21 @@ interface LocalState {
timeWindowSize: CommonAlertParams['timeWindowSize'];
timeWindowUnit: CommonAlertParams['timeWindowUnit'];
size: CommonAlertParams['size'];
excludeHitsFromPreviousRun: CommonAlertParams['excludeHitsFromPreviousRun'];
}
interface LocalStateAction {
type:
| SearchSourceParamsAction['type']
| ('threshold' | 'thresholdComparator' | 'timeWindowSize' | 'timeWindowUnit' | 'size');
payload: SearchSourceParamsAction['payload'] | (number[] | number | string);
| (
| 'threshold'
| 'thresholdComparator'
| 'timeWindowSize'
| 'timeWindowUnit'
| 'size'
| 'excludeHitsFromPreviousRun'
);
payload: SearchSourceParamsAction['payload'] | (number[] | number | string | boolean);
}
type LocalStateReducer = (prevState: LocalState, action: LocalStateAction) => LocalState;
@ -94,6 +102,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
timeWindowSize: ruleParams.timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: ruleParams.timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
size: ruleParams.size ?? DEFAULT_VALUES.SIZE,
excludeHitsFromPreviousRun:
ruleParams.excludeHitsFromPreviousRun ?? DEFAULT_VALUES.EXCLUDE_PREVIOUS_HITS,
}
);
const { index: dataView, query, filter: filters } = ruleConfiguration;
@ -173,6 +183,11 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
[]
);
const onChangeExcludeHitsFromPreviousRun = useCallback(
(exclude: boolean) => dispatch({ type: 'excludeHitsFromPreviousRun', payload: exclude }),
[]
);
const timeWindow = `${ruleConfiguration.timeWindowSize}${ruleConfiguration.timeWindowUnit}`;
const createTestSearchSource = useCallback(() => {
@ -275,6 +290,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
hasValidationErrors={hasExpressionValidationErrors(ruleParams) || !dataView}
onTestFetch={onTestFetch}
onCopyQuery={onCopyQuery}
excludeHitsFromPreviousRun={ruleConfiguration.excludeHitsFromPreviousRun}
onChangeExcludeHitsFromPreviousRun={onChangeExcludeHitsFromPreviousRun}
/>
<EuiSpacer />

View file

@ -7,7 +7,15 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSpacer, EuiTitle } from '@elastic/eui';
import {
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiIconTip,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
ForLastExpression,
@ -26,6 +34,7 @@ export interface RuleCommonExpressionsProps {
timeWindowSize: CommonAlertParams['timeWindowSize'];
timeWindowUnit: CommonAlertParams['timeWindowUnit'];
size: CommonAlertParams['size'];
excludeHitsFromPreviousRun: CommonAlertParams['excludeHitsFromPreviousRun'];
errors: IErrorObject;
hasValidationErrors: boolean;
onChangeThreshold: Parameters<typeof ThresholdExpression>[0]['onChangeSelectedThreshold'];
@ -37,6 +46,7 @@ export interface RuleCommonExpressionsProps {
onChangeSizeValue: Parameters<typeof ValueExpression>[0]['onChangeSelectedValue'];
onTestFetch: TestQueryRowProps['fetch'];
onCopyQuery?: TestQueryRowProps['copyQuery'];
onChangeExcludeHitsFromPreviousRun: (exclude: boolean) => void;
}
export const RuleCommonExpressions: React.FC<RuleCommonExpressionsProps> = ({
@ -54,6 +64,8 @@ export const RuleCommonExpressions: React.FC<RuleCommonExpressionsProps> = ({
onChangeSizeValue,
onTestFetch,
onCopyQuery,
excludeHitsFromPreviousRun,
onChangeExcludeHitsFromPreviousRun,
}) => {
return (
<>
@ -123,7 +135,21 @@ export const RuleCommonExpressions: React.FC<RuleCommonExpressionsProps> = ({
popupPosition="upLeft"
onChangeSelectedValue={onChangeSizeValue}
/>
<EuiSpacer size="s" />
<EuiSpacer size="m" />
<EuiFormRow>
<EuiCheckbox
data-test-subj="excludeHitsFromPreviousRunExpression"
checked={excludeHitsFromPreviousRun}
id="excludeHitsFromPreviousRunExpressionId"
onChange={(event) => {
onChangeExcludeHitsFromPreviousRun(event.target.checked);
}}
label={i18n.translate('xpack.stackAlerts.esQuery.ui.excludePreviousHitsExpression', {
defaultMessage: 'Exclude the hits from previous rule runs',
})}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<TestQueryRow
fetch={onTestFetch}
copyQuery={onCopyQuery}

View file

@ -59,7 +59,7 @@ export class QueryThresholdHelpPopover extends Component<{}, State> {
<p>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.thresholdHelp.timeWindow"
defaultMessage="The time window indicates how far back in time to search.
defaultMessage="The time window indicates how far back in time to search.
To avoid gaps in detection, set this value greater than or equal to the value you chose for the {checkField} field."
values={{
checkField: <b>Check every</b>,
@ -73,7 +73,7 @@ export class QueryThresholdHelpPopover extends Component<{}, State> {
size="s"
title={i18n.translate('xpack.stackAlerts.esQuery.ui.thresholdHelp.duplicateMatches', {
defaultMessage:
'If the time window is greater than the check interval and a document matches the query in multiple runs, it is used in only the first threshold calculation.',
"If the 'Exclude the hits from previous rule runs' option is checked and the time window is greater than the check interval, a document that matches the query in multiple runs will be used in only the first threshold calculation.",
})}
/>
</div>

View file

@ -29,6 +29,7 @@ export interface CommonAlertParams extends RuleTypeParams {
threshold: number[];
timeWindowSize: number;
timeWindowUnit: string;
excludeHitsFromPreviousRun: boolean;
}
export type EsQueryAlertParams<T = SearchType> = T extends SearchType.searchSource
@ -40,6 +41,7 @@ export interface OnlyEsQueryAlertParams {
index: string[];
timeField: string;
}
export interface OnlySearchSourceAlertParams {
searchType?: 'searchSource';
searchConfiguration?: SerializedSearchSourceFields;

View file

@ -25,6 +25,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
threshold: [0],
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.index.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.index[0]).toBe('Index is required.');
@ -39,6 +40,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
threshold: [0],
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.timeField.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.timeField[0]).toBe('Time field is required.');
@ -53,6 +55,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
threshold: [0],
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.esQuery[0]).toBe('Query must be valid JSON.');
@ -67,6 +70,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
threshold: [0],
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.esQuery[0]).toBe(`Query field is required.`);
@ -97,6 +101,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
thresholdComparator: '<',
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.threshold0.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.threshold0[0]).toBe('Threshold 0 is required.');
@ -112,6 +117,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
thresholdComparator: 'between',
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.threshold1[0]).toBe('Threshold 1 is required.');
@ -127,6 +133,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
thresholdComparator: 'between',
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.threshold1[0]).toBe(
@ -143,6 +150,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
threshold: [0],
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.size[0]).toBe(
@ -159,6 +167,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
threshold: [0],
timeField: '',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.size[0]).toBe(
@ -175,6 +184,7 @@ describe('expression params validation', () => {
timeWindowUnit: 's',
threshold: [0],
timeField: '@timestamp',
excludeHitsFromPreviousRun: true,
};
expect(validateExpression(initialParams).errors.size.length).toBe(0);
expect(hasExpressionValidationErrors(initialParams)).toBe(false);

View file

@ -25,6 +25,7 @@ describe('es_query executor', () => {
index: ['test-index'],
timeField: '',
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
describe('tryToParseAsDate', () => {
it.each<[string | number]>([['2019-01-01T00:00:00.000Z'], [1546300800000]])(

View file

@ -33,35 +33,36 @@ export async function fetchEsQuery(
dateEnd,
} = getSearchParams(params);
const filter = timestamp
? {
bool: {
filter: [
query,
{
bool: {
must_not: [
{
bool: {
filter: [
{
range: {
[params.timeField]: {
lte: timestamp,
format: 'strict_date_optional_time',
const filter =
timestamp && params.excludeHitsFromPreviousRun
? {
bool: {
filter: [
query,
{
bool: {
must_not: [
{
bool: {
filter: [
{
range: {
[params.timeField]: {
lte: timestamp,
format: 'strict_date_optional_time',
},
},
},
},
],
],
},
},
},
],
],
},
},
},
],
},
}
: query;
],
},
}
: query;
const sortedQuery = buildSortedEventsQuery({
index: params.index,

View file

@ -37,6 +37,7 @@ const defaultParams: OnlySearchSourceRuleParams = {
threshold: [0],
searchConfiguration: {},
searchType: 'searchSource',
excludeHitsFromPreviousRun: true,
};
describe('fetchSearchSourceQuery', () => {
@ -160,5 +161,38 @@ describe('fetchSearchSourceQuery', () => {
}
`);
});
it('does not add time range if excludeHitsFromPreviousRun is false', async () => {
const params = { ...defaultParams, excludeHitsFromPreviousRun: false };
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
const { searchSource } = updateSearchSource(
searchSourceInstance,
params,
'2020-02-09T23:12:41.941Z'
);
const searchRequest = searchSource.getSearchRequestBody();
expect(searchRequest.query).toMatchInlineSnapshot(`
Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"time": Object {
"format": "strict_date_optional_time",
"gte": "2020-02-09T23:10:41.941Z",
"lte": "2020-02-09T23:15:41.941Z",
},
},
},
],
"must": Array [],
"must_not": Array [],
"should": Array [],
},
}
`);
});
});
});

View file

@ -71,13 +71,16 @@ export function updateSearchSource(
const dateEnd = timerangeFilter?.query.range[timeFieldName].lte;
const filters = [timerangeFilter];
if (latestTimestamp && latestTimestamp > dateStart) {
// add additional filter for documents with a timestamp greater then
// the timestamp of the previous run, so that those documents are not counted twice
const field = index.fields.find((f) => f.name === timeFieldName);
const addTimeRangeField = buildRangeFilter(field!, { gt: latestTimestamp }, index);
filters.push(addTimeRangeField);
if (params.excludeHitsFromPreviousRun) {
if (latestTimestamp && latestTimestamp > dateStart) {
// add additional filter for documents with a timestamp greater then
// the timestamp of the previous run, so that those documents are not counted twice
const field = index.fields.find((f) => f.name === timeFieldName);
const addTimeRangeField = buildRangeFilter(field!, { gt: latestTimestamp }, index);
filters.push(addTimeRangeField);
}
}
const searchSourceChild = searchSource.createChild();
searchSourceChild.setField('filter', filters as Filter[]);
searchSourceChild.setField('sort', [{ [timeFieldName]: SortDirection.desc }]);

View file

@ -147,6 +147,7 @@ describe('ruleType', () => {
thresholdComparator: Comparator.BETWEEN,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -177,6 +178,7 @@ describe('ruleType', () => {
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -223,6 +225,7 @@ describe('ruleType', () => {
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -272,6 +275,7 @@ describe('ruleType', () => {
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -315,6 +319,7 @@ describe('ruleType', () => {
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -387,6 +392,7 @@ describe('ruleType', () => {
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -433,6 +439,7 @@ describe('ruleType', () => {
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -502,6 +509,7 @@ describe('ruleType', () => {
threshold: [0],
searchConfiguration: {},
searchType: 'searchSource',
excludeHitsFromPreviousRun: true,
};
afterAll(() => {

View file

@ -24,6 +24,7 @@ const DefaultParams: Writable<Partial<EsQueryRuleParams>> = {
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
excludeHitsFromPreviousRun: true,
};
describe('alertType Params validate()', () => {
@ -216,6 +217,18 @@ describe('alertType Params validate()', () => {
);
});
it('fails for invalid excludeHitsFromPreviousRun', async () => {
params.excludeHitsFromPreviousRun = '';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[excludeHitsFromPreviousRun]: expected value of type [boolean] but got [string]"`
);
});
it('uses default value "true" if excludeHitsFromPreviousRun is undefined', async () => {
params.excludeHitsFromPreviousRun = undefined;
expect(onValidate()).not.toThrow();
});
function onValidate(): () => void {
return () => validate();
}

View file

@ -31,6 +31,7 @@ export type EsQueryRuleParamsExtractedParams = Omit<EsQueryRuleParams, 'searchCo
const EsQueryRuleParamsSchemaProperties = {
size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }),
timeWindowSize: schema.number({ min: 1 }),
excludeHitsFromPreviousRun: schema.boolean({ defaultValue: true }),
timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }),
threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }),
thresholdComparator: getComparatorSchemaType(validateComparator),

View file

@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { ES_TEST_INDEX_NAME, getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
import {
createConnector,
CreateRuleParams,
ES_GROUPS_TO_WRITE,
ES_TEST_DATA_STREAM_NAME,
ES_TEST_INDEX_REFERENCE,
@ -36,7 +35,6 @@ export default function ruleTests({ getService }: FtrProviderContext) {
esTestIndexToolOutput,
esTestIndexToolDataStream,
createEsDocumentsInGroups,
waitForDocs,
} = getRuleServices(getService);
describe('rule', async () => {
@ -603,6 +601,102 @@ export default function ruleTests({ getService }: FtrProviderContext) {
})
);
describe('excludeHitsFromPreviousRun', () => {
it('excludes hits from the previous rule run when excludeHitsFromPreviousRun is true', async () => {
endDate = new Date().toISOString();
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
await createRule({
name: 'always fire',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
thresholdComparator: '>',
threshold: [0],
timeWindowSize: 300,
excludeHitsFromPreviousRun: true,
});
const docs = await waitForDocs(2);
expect(docs[0]._source.hits.length).greaterThan(0);
expect(docs[0]._source.params.message).to.match(/rule 'always fire' is active/);
expect(docs[1]._source.hits.length).to.be(0);
expect(docs[1]._source.params.message).to.match(/rule 'always fire' is recovered/);
});
it('excludes hits from the previous rule run when excludeHitsFromPreviousRun is undefined', async () => {
endDate = new Date().toISOString();
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
await createRule({
name: 'always fire',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
thresholdComparator: '>',
threshold: [0],
timeWindowSize: 300,
});
const docs = await waitForDocs(2);
expect(docs[0]._source.hits.length).greaterThan(0);
expect(docs[0]._source.params.message).to.match(/rule 'always fire' is active/);
expect(docs[1]._source.hits.length).to.be(0);
expect(docs[1]._source.params.message).to.match(/rule 'always fire' is recovered/);
});
it('does not exclude hits from the previous rule run when excludeHitsFromPreviousRun is false', async () => {
endDate = new Date().toISOString();
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
await createRule({
name: 'always fire',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
size: 100,
thresholdComparator: '>',
threshold: [0],
timeWindowSize: 300,
excludeHitsFromPreviousRun: false,
});
const docs = await waitForDocs(2);
expect(docs[0]._source.hits.length).greaterThan(0);
expect(docs[0]._source.params.message).to.match(/rule 'always fire' is active/);
expect(docs[1]._source.hits.length).greaterThan(0);
expect(docs[1]._source.params.message).to.match(/rule 'always fire' is active/);
});
});
async function waitForDocs(count: number): Promise<any[]> {
return await esTestIndexToolOutput.waitForDocs(
ES_TEST_INDEX_SOURCE,
ES_TEST_INDEX_REFERENCE,
count
);
}
interface CreateRuleParams {
name: string;
size: number;
thresholdComparator: string;
threshold: number[];
timeWindowSize?: number;
esQuery?: string;
timeField?: string;
searchConfiguration?: unknown;
searchType?: 'searchSource';
notifyWhen?: string;
indexName?: string;
excludeHitsFromPreviousRun?: boolean;
}
async function createRule(params: CreateRuleParams): Promise<string> {
const action = {
id: connectorId,
@ -676,6 +770,9 @@ export default function ruleTests({ getService }: FtrProviderContext) {
thresholdComparator: params.thresholdComparator,
threshold: params.threshold,
searchType: params.searchType,
...(params.excludeHitsFromPreviousRun !== undefined && {
excludeHitsFromPreviousRun: params.excludeHitsFromPreviousRun,
}),
...ruleParams,
},
})