mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
01ecbd48f3
commit
7d3f762186
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 |
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]])(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 }]);
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue