- setAlertThresholdPopoverOpen(false)}>
+ setAlertThresholdPopoverOpen(false)}
+ dataTestSubj="thresholdPopoverTitle"
+ >
<>{comparators[comparator].text}>
@@ -154,7 +157,7 @@ export const ThresholdExpression = ({
error={errors[`threshold${i}`] as string[]}
>
0 || isNil(threshold[i])}
diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts
index dcac33b952e3e..7f327664f1b71 100644
--- a/x-pack/test/functional/services/observability/alerts/common.ts
+++ b/x-pack/test/functional/services/observability/alerts/common.ts
@@ -6,9 +6,11 @@
*/
import expect from '@kbn/expect';
+import { ToolingLog } from '@kbn/tooling-log';
import { chunk } from 'lodash';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, AlertStatus } from '@kbn/rule-data-utils';
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
+import { Agent as SuperTestAgent } from 'supertest';
import { FtrProviderContext } from '../../../ftr_provider_context';
// Based on the x-pack/test/functional/es_archives/observability/alerts archive.
@@ -314,6 +316,69 @@ export function ObservabilityAlertsCommonProvider({
return value;
});
+ // Data view
+ const createDataView = async ({
+ supertest,
+ id,
+ name,
+ title,
+ logger,
+ }: {
+ supertest: SuperTestAgent;
+ id: string;
+ name: string;
+ title: string;
+ logger: ToolingLog;
+ }) => {
+ const { body } = await supertest
+ .post(`/api/content_management/rpc/create`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ contentTypeId: 'index-pattern',
+ data: {
+ fieldAttrs: '{}',
+ title,
+ timeFieldName: '@timestamp',
+ sourceFilters: '[]',
+ fields: '[]',
+ fieldFormatMap: '{}',
+ typeMeta: '{}',
+ runtimeFieldMap: '{}',
+ name,
+ },
+ options: { id },
+ version: 1,
+ })
+ .expect(200);
+
+ logger.debug(`Created data view: ${JSON.stringify(body)}`);
+ return body;
+ };
+
+ const deleteDataView = async ({
+ supertest,
+ id,
+ logger,
+ }: {
+ supertest: SuperTestAgent;
+ id: string;
+ logger: ToolingLog;
+ }) => {
+ const { body } = await supertest
+ .post(`/api/content_management/rpc/delete`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ contentTypeId: 'index-pattern',
+ id,
+ options: { force: true },
+ version: 1,
+ })
+ .expect(200);
+
+ logger.debug(`Deleted data view id: ${id}`);
+ return body;
+ };
+
return {
getQueryBar,
clearQueryBar,
@@ -357,5 +422,7 @@ export function ObservabilityAlertsCommonProvider({
navigateToRulesLogsPage,
navigateToRuleDetailsByRuleId,
navigateToAlertDetails,
+ createDataView,
+ deleteDataView,
};
}
diff --git a/x-pack/test/functional/services/observability/alerts/rules_page.ts b/x-pack/test/functional/services/observability/alerts/rules_page.ts
index 76f700f99b999..f5b16dc3914ab 100644
--- a/x-pack/test/functional/services/observability/alerts/rules_page.ts
+++ b/x-pack/test/functional/services/observability/alerts/rules_page.ts
@@ -7,6 +7,7 @@
import { FtrProviderContext } from '../../../ftr_provider_context';
const METRIC_THRESHOLD_RULE_TYPE_SELECTOR = 'metrics.alert.threshold-SelectOption';
+const CUSTOM_THRESHOLD_RULE_TYPE_SELECTOR = 'observability.rules.custom_threshold-SelectOption';
export function ObservabilityAlertsRulesProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
@@ -18,8 +19,9 @@ export function ObservabilityAlertsRulesProvider({ getService }: FtrProviderCont
};
const clickCreateRuleButton = async () => {
+ await testSubjects.existOrFail('createRuleButton');
const createRuleButton = await testSubjects.find('createRuleButton');
- return createRuleButton.click();
+ return await createRuleButton.click();
};
const clickRuleStatusDropDownMenu = async () => testSubjects.click('statusDropdown');
@@ -33,6 +35,7 @@ export function ObservabilityAlertsRulesProvider({ getService }: FtrProviderCont
};
const clickOnInfrastructureCategory = async () => {
+ await testSubjects.existOrFail('ruleTypeModal');
const categories = await testSubjects.find('ruleTypeModal');
const category = await categories.findByCssSelector(`.euiFacetButton[title="Infrastructure"]`);
await category.click();
@@ -43,6 +46,18 @@ export function ObservabilityAlertsRulesProvider({ getService }: FtrProviderCont
await testSubjects.click(METRIC_THRESHOLD_RULE_TYPE_SELECTOR);
};
+ const clickOnObservabilityCategory = async () => {
+ await testSubjects.existOrFail('ruleTypeModal');
+ const categories = await testSubjects.find('ruleTypeModal');
+ const category = await categories.findByCssSelector(`.euiFacetButton[title="Observability"]`);
+ await category.click();
+ };
+
+ const clickOnCustomThresholdRule = async () => {
+ await testSubjects.existOrFail(CUSTOM_THRESHOLD_RULE_TYPE_SELECTOR);
+ await testSubjects.click(CUSTOM_THRESHOLD_RULE_TYPE_SELECTOR);
+ };
+
return {
getManageRulesPageHref,
clickCreateRuleButton,
@@ -52,5 +67,7 @@ export function ObservabilityAlertsRulesProvider({ getService }: FtrProviderCont
clickOnRuleInEventLogs,
clickOnInfrastructureCategory,
clickOnMetricThresholdRule,
+ clickOnObservabilityCategory,
+ clickOnCustomThresholdRule,
};
}
diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts
index f16689bb3d22f..8c176c61530b7 100644
--- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts
@@ -163,7 +163,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return ruleName === alertName;
});
await testSubjects.click('thresholdPopover');
- await testSubjects.setValue('alertThresholdInput', '1');
+ await testSubjects.setValue('alertThresholdInput0', '1');
await testSubjects.click('forLastExpression');
await testSubjects.setValue('timeWindowSizeNumber', '30');
@@ -469,7 +469,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await filterBar.addFilter({ field: 'message.keyword', operation: 'is', value: 'msg-1' });
await testSubjects.click('thresholdPopover');
- await testSubjects.setValue('alertThresholdInput', '1');
+ await testSubjects.setValue('alertThresholdInput0', '1');
await testSubjects.click('saveEditedRuleButton');
await PageObjects.header.waitUntilLoadingHasFinished();
diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts
index 64636e79123d7..96256248e5d88 100644
--- a/x-pack/test/observability_functional/apps/observability/index.ts
+++ b/x-pack/test/observability_functional/apps/observability/index.ts
@@ -17,6 +17,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./pages/alerts/rule_stats'));
loadTestFile(require.resolve('./pages/alerts/state_synchronization'));
loadTestFile(require.resolve('./pages/alerts/table_storage'));
+ loadTestFile(require.resolve('./pages/alerts/custom_threshold'));
loadTestFile(require.resolve('./pages/cases/case_details'));
loadTestFile(require.resolve('./pages/overview/alert_table'));
loadTestFile(require.resolve('./exploratory_view'));
diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold.ts
new file mode 100644
index 0000000000000..38d308a17e7b0
--- /dev/null
+++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold.ts
@@ -0,0 +1,254 @@
+/*
+ * 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 { Key } from 'selenium-webdriver';
+import expect from 'expect';
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export default ({ getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const testSubjects = getService('testSubjects');
+ const kibanaServer = getService('kibanaServer');
+ const supertest = getService('supertest');
+ const find = getService('find');
+ const logger = getService('log');
+ const retry = getService('retry');
+
+ describe('Custom threshold rule', function () {
+ this.tags('includeFirefox');
+
+ const observability = getService('observability');
+ const DATA_VIEW_1 = 'filebeat-*';
+ const DATA_VIEW_1_ID = 'data-view-id_1';
+ const DATA_VIEW_1_NAME = 'test-data-view-name_1';
+ const DATA_VIEW_2 = 'metricbeat-*';
+ const DATA_VIEW_2_ID = 'data-view-id_2';
+ const DATA_VIEW_2_NAME = 'test-data-view-name_2';
+
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ // create two data views
+ await observability.alerts.common.createDataView({
+ supertest,
+ name: DATA_VIEW_1_NAME,
+ id: DATA_VIEW_1_ID,
+ title: DATA_VIEW_1,
+ logger,
+ });
+ await observability.alerts.common.createDataView({
+ supertest,
+ name: DATA_VIEW_2_NAME,
+ id: DATA_VIEW_2_ID,
+ title: DATA_VIEW_2,
+ logger,
+ });
+ await observability.alerts.common.navigateToRulesPage();
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ // This also deletes the created data views
+ await kibanaServer.savedObjects.cleanStandardList();
+ });
+
+ it('shows the custom threshold rule in the observability section', async () => {
+ await observability.alerts.rulesPage.clickCreateRuleButton();
+ await observability.alerts.rulesPage.clickOnObservabilityCategory();
+ await observability.alerts.rulesPage.clickOnCustomThresholdRule();
+ });
+
+ it('can add name and tags', async () => {
+ await testSubjects.setValue('ruleNameInput', 'test custom threshold rule');
+ await testSubjects.setValue('comboBoxSearchInput', 'tag1');
+ });
+
+ it('can add data view', async () => {
+ // select data view
+ await testSubjects.click('selectDataViewExpression');
+ await testSubjects.setValue('indexPattern-switcher--input', 'test-data-view-name_2');
+ const dataViewExpression = await find.byCssSelector(
+ '[data-test-subj="indexPattern-switcher--input"]'
+ );
+ await dataViewExpression.pressKeys(Key.ENTER);
+ await retry.waitFor('data view selection to happen', async () => {
+ const dataViewSelector = await testSubjects.find('selectDataViewExpression');
+ return (await dataViewSelector.getVisibleText()) === 'DATA VIEW\ntest-data-view-name_2';
+ });
+ });
+
+ it('can select aggregation', async () => {
+ // select aggregation
+ await testSubjects.click('aggregationNameA');
+ await testSubjects.click('aggregationTypeSelect');
+ // assert all options are available
+ await find.byCssSelector('option[value="avg"]');
+ await find.byCssSelector('option[value="min"]');
+ await find.byCssSelector('option[value="max"]');
+ await find.byCssSelector('option[value="sum"]');
+ await find.byCssSelector('option[value="count"]');
+ await find.byCssSelector('option[value="cardinality"]');
+ await find.byCssSelector('option[value="p99"]');
+ await find.byCssSelector('option[value="p95"]');
+ await find.byCssSelector('option[value="rate"]');
+
+ // set first aggregation
+ await find.clickByCssSelector(`option[value="avg"]`);
+ const input1 = await find.byCssSelector('[data-test-subj="aggregationField"] input');
+ await input1.type('metricset.rtt');
+ await testSubjects.click('o11yClosablePopoverTitleButton');
+ await retry.waitFor('first aggregation to happen', async () => {
+ const aggregationNameA = await testSubjects.find('aggregationNameA');
+ return (await aggregationNameA.getVisibleText()) === 'AVERAGE\nmetricset.rtt';
+ });
+ await new Promise((r) => setTimeout(r, 1000));
+
+ // set second aggregation
+ await testSubjects.click('thresholdRuleCustomEquationEditorAddAggregationFieldButton');
+ await testSubjects.click('aggregationNameB');
+ await testSubjects.setValue('o11ySearchField', 'service.name : "opbeans-node"');
+ await testSubjects.click('o11yClosablePopoverTitleButton');
+ await retry.waitFor('first aggregation to happen', async () => {
+ const aggregationNameB = await testSubjects.find('aggregationNameB');
+ return (await aggregationNameB.getVisibleText()) === 'COUNT\nservice.name : "opbeans-node"';
+ });
+ await new Promise((r) => setTimeout(r, 1000));
+ });
+
+ it('can set custom equation', async () => {
+ // set custom equation
+ await testSubjects.click('customEquation');
+ const customEquationField = await find.byCssSelector(
+ '[data-test-subj="thresholdRuleCustomEquationEditorFieldText"]'
+ );
+ await customEquationField.click();
+ await customEquationField.type('A - B');
+ await testSubjects.click('o11yClosablePopoverTitleButton');
+ await retry.waitFor('custom equation update to happen', async () => {
+ const customEquation = await testSubjects.find('customEquation');
+ return (await customEquation.getVisibleText()) === 'EQUATION\nA - B';
+ });
+ await new Promise((r) => setTimeout(r, 1000));
+ });
+
+ it('can set threshold', async () => {
+ // set threshold
+ await testSubjects.click('thresholdPopover');
+ await testSubjects.click('comparatorOptionsComboBox');
+ // assert all options are available
+ await find.byCssSelector('option[value=">="]');
+ await find.byCssSelector('option[value="<="]');
+ await find.byCssSelector('option[value=">"]');
+ await find.byCssSelector('option[value="<"]');
+ await find.byCssSelector('option[value="between"]');
+ await find.byCssSelector('option[value="notBetween"]');
+ // select an option
+ await find.clickByCssSelector(`option[value="notBetween"]`);
+ const thresholdField1 = await find.byCssSelector('[data-test-subj="alertThresholdInput0"]');
+ await thresholdField1.click();
+ await new Promise((r) => setTimeout(r, 1000));
+ await thresholdField1.pressKeys(Key.BACK_SPACE);
+ await new Promise((r) => setTimeout(r, 1000));
+ await thresholdField1.pressKeys(Key.BACK_SPACE);
+ await new Promise((r) => setTimeout(r, 1000));
+ await thresholdField1.pressKeys(Key.BACK_SPACE);
+ await thresholdField1.type('200');
+ const thresholdField2 = await find.byCssSelector('[data-test-subj="alertThresholdInput1"]');
+ await thresholdField2.type('250');
+ await find.clickByCssSelector('[aria-label="Close"]');
+ await retry.waitFor('comparator selection to happen', async () => {
+ const customEquation = await testSubjects.find('thresholdPopover');
+ return (await customEquation.getVisibleText()) === 'IS NOT BETWEEN\n200 AND 250';
+ });
+ });
+
+ it('can set equation label', async () => {
+ // set equation label
+ await testSubjects.setValue(
+ 'thresholdRuleCustomEquationEditorFieldTextLabel',
+ 'test equation'
+ );
+ });
+
+ it('can set time range', async () => {
+ // set time range
+ await testSubjects.click('forLastExpression');
+ await new Promise((r) => setTimeout(r, 1000));
+ const timeRangeField = await find.byCssSelector('[data-test-subj="timeWindowSizeNumber"]');
+ await timeRangeField.click();
+ await new Promise((r) => setTimeout(r, 1000));
+ await timeRangeField.pressKeys(Key.BACK_SPACE);
+ await timeRangeField.type('2');
+ // assert all options are available
+ await testSubjects.click('timeWindowUnitSelect');
+ await find.byCssSelector('option[value="s"]');
+ await find.byCssSelector('option[value="m"]');
+ await find.byCssSelector('option[value="h"]');
+ await find.byCssSelector('option[value="d"]');
+ // select an option
+ await new Promise((r) => setTimeout(r, 3000));
+ await find.clickByCssSelector('[data-test-subj="timeWindowUnitSelect"] option[value="d"]');
+ await find.clickByCssSelector('[aria-label="Close"]');
+ });
+
+ it('can set groupby', async () => {
+ // set group by
+ const groupByField = await find.byCssSelector(
+ '[data-test-subj="thresholdRuleMetricsExplorer-groupBy"] [data-test-subj="comboBoxSearchInput"]'
+ );
+ await groupByField.type('docker.container.name');
+ });
+
+ it('can save the rule', async () => {
+ await testSubjects.click('saveRuleButton');
+ await testSubjects.click('confirmModalConfirmButton');
+ await find.byCssSelector('button[title="test custom threshold rule"]');
+ });
+
+ it('saved the rule correctly', async () => {
+ const { body: rules } = await supertest.get('/internal/alerting/rules/_find');
+
+ expect(rules.data.length).toEqual(1);
+ expect(rules.data[0]).toEqual(
+ expect.objectContaining({
+ name: 'test custom threshold rule',
+ tags: ['tag1'],
+ params: expect.objectContaining({
+ alertOnGroupDisappear: false,
+ alertOnNoData: false,
+ criteria: [
+ {
+ comparator: 'notBetween',
+ label: 'test equation',
+ equation: 'A - B',
+ metrics: [
+ {
+ aggType: 'avg',
+ field: 'metricset.rtt',
+ name: 'A',
+ },
+ {
+ aggType: 'count',
+ filter: 'service.name : "opbeans-node"',
+ name: 'B',
+ },
+ ],
+ threshold: [200, 250],
+ timeSize: 2,
+ timeUnit: 'd',
+ },
+ ],
+ groupBy: ['docker.container.name'],
+ searchConfiguration: {
+ index: 'data-view-id_2',
+ query: { query: '', language: 'kuery' },
+ },
+ }),
+ })
+ );
+ });
+ });
+};
diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index_threshold_rule.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index_threshold_rule.ts
index 1155cf79a8e27..7d0b283a1f028 100644
--- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index_threshold_rule.ts
+++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index_threshold_rule.ts
@@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await nameInput1.click();
await testSubjects.click('thresholdPopover');
- await testSubjects.setValue('alertThresholdInput', '420000');
+ await testSubjects.setValue('alertThresholdInput0', '420000');
await testSubjects.click('forLastExpression');
await testSubjects.setValue('timeWindowSizeNumber', '24');
await testSubjects.setValue('timeWindowUnitSelect', 'hours');
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts
index 897c67c717c05..d7f9e6f7450e6 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts
@@ -195,7 +195,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return ruleName === alertName;
});
await testSubjects.click('thresholdPopover');
- await testSubjects.setValue('alertThresholdInput', '1');
+ await testSubjects.setValue('alertThresholdInput0', '1');
await testSubjects.click('forLastExpression');
await testSubjects.setValue('timeWindowSizeNumber', '30');
@@ -499,7 +499,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await filterBar.addFilter({ field: 'message.keyword', operation: 'is', value: 'msg-1' });
await testSubjects.click('thresholdPopover');
- await testSubjects.setValue('alertThresholdInput', '1');
+ await testSubjects.setValue('alertThresholdInput0', '1');
await testSubjects.click('saveEditedRuleButton');
await PageObjects.header.waitUntilLoadingHasFinished();