From d08ad0c4377d6746b80986bf75936d9d4835db71 Mon Sep 17 00:00:00 2001
From: Maxim Palenov
Date: Wed, 18 Oct 2023 18:26:31 +0200
Subject: [PATCH 01/14] [Security Solution] Unskip rules export import
serverless cypress tests (#169230)
**Addresses:** https://github.com/elastic/kibana/issues/161540
## Summary
This PR unskips import and export security rules Serverless Cypress tests.
## Flaky test runner
`import_export` folder [150 runs](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3588)
---
.../rule_actions/import_export/export_rule.cy.ts | 6 ++----
.../rule_actions/import_export/import_rules.cy.ts | 12 +++++++-----
.../cypress/tasks/alerts_detection_rules.ts | 6 +++---
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts
index 262c6904db4bd..275160d1f04a0 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/export_rule.cy.ts
@@ -33,7 +33,6 @@ import {
cleanKibana,
resetRulesTableState,
deleteAlertsAndRules,
- reload,
} from '../../../../../tasks/common';
import { login } from '../../../../../tasks/login';
import { visit } from '../../../../../tasks/navigation';
@@ -56,8 +55,7 @@ const prebuiltRules = Array.from(Array(7)).map((_, i) => {
});
});
-// TODO: https://github.com/elastic/kibana/issues/161540
-describe('Export rules', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
+describe('Export rules', { tags: ['@ess', '@serverless'] }, () => {
const downloadsFolder = Cypress.config('downloadsFolder');
before(() => {
@@ -171,7 +169,7 @@ describe('Export rules', { tags: ['@ess', '@serverless', '@brokenInServerless']
const expectedNumberCustomRulesToBeExported = 2;
createAndInstallMockedPrebuiltRules(prebuiltRules);
- reload();
+ cy.reload();
selectAllRules();
bulkExportRules();
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/import_rules.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/import_rules.cy.ts
index 6c0bbb0193130..ed8a2ddc28c07 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/import_rules.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/import_export/import_rules.cy.ts
@@ -11,15 +11,16 @@ import {
importRules,
importRulesWithOverwriteAll,
} from '../../../../../tasks/alerts_detection_rules';
-import { cleanKibana, deleteAlertsAndRules, reload } from '../../../../../tasks/common';
+import { cleanKibana, deleteAlertsAndRules } from '../../../../../tasks/common';
+import { deleteExceptionList } from '../../../../../tasks/api_calls/exceptions';
import { login } from '../../../../../tasks/login';
import { visit } from '../../../../../tasks/navigation';
import { RULES_MANAGEMENT_URL } from '../../../../../urls/rules_management';
const RULES_TO_IMPORT_FILENAME = 'cypress/fixtures/7_16_rules.ndjson';
+const IMPORTED_EXCEPTION_ID = 'b8dfd17f-1e11-41b0-ae7e-9e7f8237de49';
-// TODO: https://github.com/elastic/kibana/issues/161540
-describe('Import rules', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
+describe('Import rules', { tags: ['@ess', '@serverless'] }, () => {
before(() => {
cleanKibana();
});
@@ -27,6 +28,7 @@ describe('Import rules', { tags: ['@ess', '@serverless', '@brokenInServerless']
beforeEach(() => {
login();
deleteAlertsAndRules();
+ deleteExceptionList(IMPORTED_EXCEPTION_ID, 'single');
cy.intercept('POST', '/api/detection_engine/rules/_import*').as('import');
visit(RULES_MANAGEMENT_URL);
});
@@ -52,7 +54,7 @@ describe('Import rules', { tags: ['@ess', '@serverless', '@brokenInServerless']
cy.wrap(response?.statusCode).should('eql', 200);
});
- reload();
+ cy.reload();
importRules(RULES_TO_IMPORT_FILENAME);
cy.wait('@import').then(({ response }) => {
@@ -68,7 +70,7 @@ describe('Import rules', { tags: ['@ess', '@serverless', '@brokenInServerless']
cy.wrap(response?.statusCode).should('eql', 200);
});
- reload();
+ cy.reload();
importRulesWithOverwriteAll(RULES_TO_IMPORT_FILENAME);
cy.wait('@import').then(({ response }) => {
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts
index 304e35e6826bf..d5eeef7801d1b 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts
@@ -285,11 +285,11 @@ export const waitForRuleToUpdate = () => {
export const importRules = (rulesFile: string) => {
cy.get(RULE_IMPORT_MODAL).click();
- cy.get(INPUT_FILE).click({ force: true });
+ cy.get(INPUT_FILE).click();
cy.get(INPUT_FILE).selectFile(rulesFile);
cy.get(INPUT_FILE).trigger('change');
- cy.get(RULE_IMPORT_MODAL_BUTTON).last().click({ force: true });
- cy.get(INPUT_FILE).should('not.exist');
+ cy.get(RULE_IMPORT_MODAL_BUTTON).last().click();
+ cy.get(INPUT_FILE, { timeout: 300000 }).should('not.exist');
};
export const expectRulesManagementTab = () => {
From 3c58d392c0005c49deabe6e08f8906eccbea94e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Wed, 18 Oct 2023 18:11:53 +0100
Subject: [PATCH 02/14] [Cloud] Endpoint modal - fix label height (#169249)
---
.../deployment_details/deployment_details_cloudid_input.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx b/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx
index 802e11aa05fcf..aa7b13ef8282a 100644
--- a/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx
+++ b/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx
@@ -32,7 +32,7 @@ const Label: FC<{ learnMoreUrl: string }> = ({ learnMoreUrl }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
return (
-
+
{i18n.translate('cloud.deploymentDetails.cloudIDLabel', {
From 121353cf457efe957662bf18d867ba76ffb4100e Mon Sep 17 00:00:00 2001
From: Cee Chen <549407+cee-chen@users.noreply.github.com>
Date: Wed, 18 Oct 2023 10:18:42 -0700
Subject: [PATCH 03/14] [rollup][licensing_management][grokdebugger] Migrate
deprecated `EuiPage*` components (#168300)
## Summary
EUI will shortly be removing several deprecated `EuiPage*` components,
and we're updating a few remaining Kibana usages of these deprecated
components ahead of time.
See https://github.com/elastic/kibana/issues/161872 for other similar
tasks with more information about this effort.
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/grok_debugger/grok_debugger.js | 60 +-
.../public/components/inactive_license.js | 87 +-
.../upload_license.test.tsx.snap | 993 +++++++++---------
.../public/application/app.js | 20 +-
.../license_dashboard/license_dashboard.js | 10 +-
.../sections/upload_license/upload_license.js | 12 +-
.../sections/job_create/job_create.js | 6 +-
.../crud_app/sections/job_list/job_list.js | 21 +-
8 files changed, 604 insertions(+), 605 deletions(-)
diff --git a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js
index 7eb4e9412edaa..cfae6749879f3 100644
--- a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js
+++ b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js
@@ -11,15 +11,7 @@ import { i18n } from '@kbn/i18n';
// eslint-disable-next-line no-restricted-imports
import isEmpty from 'lodash/isEmpty';
-import {
- EuiForm,
- EuiButton,
- EuiPage,
- EuiPageBody,
- EuiPageContent_Deprecated as EuiPageContent,
- EuiPageContentBody_Deprecated as EuiPageContentBody,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiForm, EuiButton, EuiPage, EuiPageBody, EuiPageSection, EuiSpacer } from '@elastic/eui';
import { EventInput } from '../event_input';
import { PatternInput } from '../pattern_input';
import { CustomPatternsInput } from '../custom_patterns_input';
@@ -126,33 +118,31 @@ export class GrokDebuggerComponent extends React.Component {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
diff --git a/x-pack/plugins/grokdebugger/public/components/inactive_license.js b/x-pack/plugins/grokdebugger/public/components/inactive_license.js
index f373c65bf9bb1..b9abdd6a4706f 100644
--- a/x-pack/plugins/grokdebugger/public/components/inactive_license.js
+++ b/x-pack/plugins/grokdebugger/public/components/inactive_license.js
@@ -13,8 +13,7 @@ import {
EuiCode,
EuiPage,
EuiPageBody,
- EuiPageContent_Deprecated as EuiPageContent,
- EuiPageContentBody_Deprecated as EuiPageContentBody,
+ EuiPageSection,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -43,49 +42,47 @@ export const InactiveLicenseSlate = () => {
return (
-
-
-
-
-
-
- {trialLicense}, {basicLicense},{' '}
- {goldLicense}
- >
- ),
- platinumLicenseType: {platinumLicense},
- }}
- />
-
-
-
- {registerLicenseLinkLabel}
-
- ),
- }}
- />
-
-
-
-
-
+
+
+
+
+
+ {trialLicense}, {basicLicense},{' '}
+ {goldLicense}
+ >
+ ),
+ platinumLicenseType: {platinumLicense},
+ }}
+ />
+
+
+
+ {registerLicenseLinkLabel}
+
+ ),
+ }}
+ />
+
+
+
+
);
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
index 1ce5f5a38b043..8a845149b95e8 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
@@ -1,675 +1,690 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UploadLicense should display a modal when license requires acknowledgement 1`] = `
-
-
- Upload your license
-
-
-
-
- Your license key is a JSON file with a signature attached.
-
-
- Uploading a license will replace your current
-
- license.
-
-
-
-
+
`;
exports[`UploadLicense should display an error when ES says license is expired 1`] = `
-
-
- Upload your license
-
-
-
-
- Your license key is a JSON file with a signature attached.
-
-
- Uploading a license will replace your current
-
- license.
-
-
-
-
+
`;
exports[`UploadLicense should display an error when ES says license is invalid 1`] = `
-
-
- Upload your license
-
-
-
- Your license key is a JSON file with a signature attached.
-
-
- Uploading a license will replace your current
-
- license.
-
-
-
-
-
+
`;
exports[`UploadLicense should display an error when submitting invalid JSON 1`] = `
-
-
- Upload your license
-
-
-
- Your license key is a JSON file with a signature attached.
-
-
- Uploading a license will replace your current
-
- license.
-
-
-
-
-
+
`;
exports[`UploadLicense should display error when ES returns error 1`] = `
-
-
- Upload your license
-
-
-
-
- Your license key is a JSON file with a signature attached.
-
-
- Uploading a license will replace your current
-
- license.
-
-
-
-
+
`;
diff --git a/x-pack/plugins/license_management/public/application/app.js b/x-pack/plugins/license_management/public/application/app.js
index e1b2ea7fb6b2e..8932193b01f4a 100644
--- a/x-pack/plugins/license_management/public/application/app.js
+++ b/x-pack/plugins/license_management/public/application/app.js
@@ -11,11 +11,7 @@ import { LicenseDashboard, UploadLicense } from './sections';
import { Routes, Route } from '@kbn/shared-ux-router';
import { APP_PERMISSION } from '../../common/constants';
import { SectionLoading, useExecutionContext } from '../shared_imports';
-import {
- EuiPageContent_Deprecated as EuiPageContent,
- EuiPageBody,
- EuiEmptyPrompt,
-} from '@elastic/eui';
+import { EuiPageSection, EuiPageBody, EuiEmptyPrompt } from '@elastic/eui';
import { UPLOAD_LICENSE_ROUTE } from '../locator';
export const App = ({
@@ -37,14 +33,14 @@ export const App = ({
if (permissionsLoading) {
return (
-
+
-
+
);
}
@@ -52,8 +48,9 @@ export const App = ({
const error = permissionsError?.data?.message;
return (
-
+
@@ -65,14 +62,15 @@ export const App = ({
}
body={error ? {error}
: null}
/>
-
+
);
}
if (!hasPermission) {
return (
-
+
@@ -94,7 +92,7 @@ export const App = ({
}
/>
-
+
);
}
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.js
index ce5048679ffeb..d412415752944 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_dashboard.js
@@ -6,11 +6,7 @@
*/
import React, { useEffect } from 'react';
-import {
- EuiPageContentBody_Deprecated as EuiPageContentBody,
- EuiFlexGroup,
- EuiFlexItem,
-} from '@elastic/eui';
+import { EuiPageSection, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { StartTrial } from './start_trial';
import { LicensePageHeader } from './license_page_header';
@@ -27,7 +23,7 @@ export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb:
<>
-
+
@@ -36,7 +32,7 @@ export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb:
-
+
>
);
};
diff --git a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js
index 09b076cc4efc9..b637b9a29f2fc 100644
--- a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js
+++ b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js
@@ -17,8 +17,8 @@ import {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
- EuiPageContent_Deprecated as EuiPageContent,
- EuiPageContentBody_Deprecated as EuiPageContentBody,
+ EuiPageSection,
+ EuiPanel,
} from '@elastic/eui';
import { TelemetryOptIn } from '../../components/telemetry_opt_in';
import { shouldShowTelemetryOptIn } from '../../lib/telemetry';
@@ -126,8 +126,8 @@ export class UploadLicense extends React.PureComponent {
const { currentLicenseType, applying, telemetry, history } = this.props;
return (
-
-
+
+
-
-
+
+
);
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
index a0defbc7e1d9f..c782e8646b714 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
@@ -18,7 +18,7 @@ import {
EuiCallOut,
EuiLoadingLogo,
EuiOverlayMask,
- EuiPageContentBody_Deprecated as EuiPageContentBody,
+ EuiPageSection,
EuiPageHeader,
EuiSpacer,
EuiStepsHorizontal,
@@ -543,7 +543,7 @@ export class JobCreateUi extends Component {
}
return (
-
+
+
);
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
index 89ef725ad05ae..83641d6483817 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
@@ -15,7 +15,7 @@ import {
EuiButtonEmpty,
EuiEmptyPrompt,
EuiPageHeader,
- EuiPageContent_Deprecated as EuiPageContent,
+ EuiPageSection,
EuiSpacer,
} from '@elastic/eui';
@@ -87,8 +87,9 @@ export class JobListUi extends Component {
defaultMessage: 'Permission error',
});
return (
-
+
{title}}
@@ -101,7 +102,7 @@ export class JobListUi extends Component {
}
/>
-
+
);
}
@@ -115,8 +116,9 @@ export class JobListUi extends Component {
});
return (
-
+
{title}}
@@ -126,14 +128,15 @@ export class JobListUi extends Component {
}
/>
-
+
);
}
renderEmpty() {
return (
-
+
}
/>
-
+
);
}
renderLoading() {
return (
-
+
-
+
);
}
From 7df3f964ce31285162dd8bc7d3850691872d01d5 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau
Date: Wed, 18 Oct 2023 14:03:47 -0400
Subject: [PATCH 04/14] [RAM] fix Slack API proxy (#169171)
## Summary
FIX -> https://github.com/elastic/kibana/issues/168701
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../actions/server/lib/axios_utils.test.ts | 21 +++++++++++++++-
.../plugins/actions/server/lib/axios_utils.ts | 5 ++++
.../connector_types/slack_api/index.test.ts | 10 ++++++--
.../connector_types/slack_api/service.test.ts | 24 +++++++++++++++----
.../connector_types/slack_api/service.ts | 18 +++++++-------
.../server/routes/valid_slack_api_channels.ts | 14 +++++------
6 files changed, 68 insertions(+), 24 deletions(-)
diff --git a/x-pack/plugins/actions/server/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/lib/axios_utils.test.ts
index 0cbc3cdde0046..43f16b4863e9a 100644
--- a/x-pack/plugins/actions/server/lib/axios_utils.test.ts
+++ b/x-pack/plugins/actions/server/lib/axios_utils.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import axios from 'axios';
+import axios, { AxiosInstance } from 'axios';
import { Agent as HttpsAgent } from 'https';
import HttpProxyAgent from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
@@ -320,6 +320,25 @@ describe('request', () => {
expect(axiosMock.mock.calls[0][1].timeout).toBe(360000);
expect(axiosMock.mock.calls[1][1].timeout).toBe(360001);
});
+
+ test('throw an error if you use baseUrl in your axios instance', async () => {
+ await expect(async () => {
+ await request({
+ axios: {
+ ...axios,
+ defaults: {
+ ...axios.defaults,
+ baseURL: 'https://here-we-go.com',
+ },
+ } as unknown as AxiosInstance,
+ url: '/test',
+ logger,
+ configurationUtilities,
+ });
+ }).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Do not use \\"baseURL\\" in the creation of your axios instance because you will mostly break proxy"`
+ );
+ });
});
describe('patch', () => {
diff --git a/x-pack/plugins/actions/server/lib/axios_utils.ts b/x-pack/plugins/actions/server/lib/axios_utils.ts
index bed2e512761a0..b623f427be681 100644
--- a/x-pack/plugins/actions/server/lib/axios_utils.ts
+++ b/x-pack/plugins/actions/server/lib/axios_utils.ts
@@ -41,6 +41,11 @@ export const request = async ({
timeout?: number;
sslOverrides?: SSLSettings;
} & AxiosRequestConfig): Promise => {
+ if (!isEmpty(axios?.defaults?.baseURL ?? '')) {
+ throw new Error(
+ `Do not use "baseURL" in the creation of your axios instance because you will mostly break proxy`
+ );
+ }
const { httpAgent, httpsAgent } = getCustomAgents(
configurationUtilities,
logger,
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/index.test.ts
index 62e7cf771d3cb..37f0209e6f393 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/index.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/index.test.ts
@@ -32,6 +32,10 @@ const requestMock = utils.request as jest.Mock;
const services: Services = actionsMock.createServices();
const mockedLogger: jest.Mocked = loggerMock.create();
+const headers = {
+ Authorization: 'Bearer some token',
+ 'Content-type': 'application/json; charset=UTF-8',
+};
let connectorType: SlackApiConnectorType;
let configurationUtilities: jest.Mocked;
@@ -266,9 +270,10 @@ describe('execute', () => {
expect(requestMock).toHaveBeenCalledWith({
axios,
configurationUtilities,
+ headers,
logger: mockedLogger,
method: 'post',
- url: 'chat.postMessage',
+ url: 'https://slack.com/api/chat.postMessage',
data: { channel: 'general', text: 'some text' },
});
@@ -317,9 +322,10 @@ describe('execute', () => {
expect(requestMock).toHaveBeenCalledWith({
axios,
configurationUtilities,
+ headers,
logger: mockedLogger,
method: 'get',
- url: 'conversations.info?channel=ZXCVBNM567',
+ url: 'https://slack.com/api/conversations.info?channel=ZXCVBNM567',
});
expect(response).toEqual({
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.test.ts
index 068bd8d5d923e..fa3e5d01b79d8 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.test.ts
@@ -116,10 +116,14 @@ describe('Slack API service', () => {
await service.validChannelId('channel_id_1');
expect(requestMock).toHaveBeenCalledWith({
axios,
+ headers: {
+ Authorization: 'Bearer token',
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
logger,
configurationUtilities,
method: 'get',
- url: 'conversations.info?channel=channel_id_1',
+ url: 'https://slack.com/api/conversations.info?channel=channel_id_1',
});
});
@@ -146,10 +150,14 @@ describe('Slack API service', () => {
expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenNthCalledWith(1, {
axios,
+ headers: {
+ Authorization: 'Bearer token',
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
logger,
configurationUtilities,
method: 'post',
- url: 'chat.postMessage',
+ url: 'https://slack.com/api/chat.postMessage',
data: { channel: 'general', text: 'a message' },
});
});
@@ -166,10 +174,14 @@ describe('Slack API service', () => {
expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenNthCalledWith(1, {
axios,
+ headers: {
+ Authorization: 'Bearer token',
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
logger,
configurationUtilities,
method: 'post',
- url: 'chat.postMessage',
+ url: 'https://slack.com/api/chat.postMessage',
data: { channel: 'QWEERTYU987', text: 'a message' },
});
});
@@ -183,9 +195,13 @@ describe('Slack API service', () => {
expect(requestMock).toHaveBeenNthCalledWith(1, {
axios,
logger,
+ headers: {
+ Authorization: 'Bearer token',
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
configurationUtilities,
method: 'post',
- url: 'chat.postMessage',
+ url: 'https://slack.com/api/chat.postMessage',
data: { channel: 'QWEERTYU987', text: 'a message' },
});
});
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts
index f4ecb95571257..63746cc85dc78 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts
@@ -120,13 +120,11 @@ export const createExternalService = (
throw Error(`[Action][${SLACK_CONNECTOR_NAME}]: Wrong configuration.`);
}
- const axiosInstance = axios.create({
- baseURL: SLACK_URL,
- headers: {
- Authorization: `Bearer ${token}`,
- 'Content-type': 'application/json; charset=UTF-8',
- },
- });
+ const axiosInstance = axios.create();
+ const headers = {
+ Authorization: `Bearer ${token}`,
+ 'Content-type': 'application/json; charset=UTF-8',
+ };
const validChannelId = async (
channelId: string
@@ -138,7 +136,8 @@ export const createExternalService = (
configurationUtilities,
logger,
method: 'get',
- url: `conversations.info?channel=${channelId}`,
+ headers,
+ url: `${SLACK_URL}conversations.info?channel=${channelId}`,
});
};
if (channelId.length === 0) {
@@ -198,9 +197,10 @@ export const createExternalService = (
const result: AxiosResponse = await request({
axios: axiosInstance,
method: 'post',
- url: 'chat.postMessage',
+ url: `${SLACK_URL}chat.postMessage`,
logger,
data: { channel: channelToUse, text },
+ headers,
configurationUtilities,
});
diff --git a/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts b/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts
index 434f989f56e92..cd2cb113a6750 100644
--- a/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts
+++ b/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts
@@ -48,13 +48,7 @@ export const validSlackApiChannelsRoute = (
): Promise {
const { authToken, channelIds } = req.body;
- const axiosInstance = axios.create({
- baseURL: SLACK_URL,
- headers: {
- Authorization: `Bearer ${authToken}`,
- 'Content-type': 'application/json; charset=UTF-8',
- },
- });
+ const axiosInstance = axios.create();
const validChannelId = (
channelId: string = ''
@@ -62,9 +56,13 @@ export const validSlackApiChannelsRoute = (
return request({
axios: axiosInstance,
configurationUtilities,
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
logger,
method: 'get',
- url: `conversations.info?channel=${channelId}`,
+ url: `${SLACK_URL}conversations.info?channel=${channelId}`,
});
};
From 8284398023648e850a8ca038bce8be6cc85cc51f Mon Sep 17 00:00:00 2001
From: Steph Milovic
Date: Wed, 18 Oct 2023 12:06:03 -0600
Subject: [PATCH 05/14] [Security solution] Fix OpenAI token reporting
(#169156)
---
.../impl/assistant/api.test.tsx | 17 +++++---
.../impl/assistant/api.tsx | 7 +--
.../server/__mocks__/action_result_data.ts | 5 ++-
.../elastic_assistant/server/lib/executor.ts | 43 +++++++++++++++++++
.../langchain/llm/actions_client_llm.test.ts | 5 ++-
.../lib/langchain/llm/actions_client_llm.ts | 3 +-
.../server/routes/evaluate/post_evaluate.ts | 1 +
.../post_actions_connector_execute.test.ts | 41 +++++++++++++++++-
.../routes/post_actions_connector_execute.ts | 9 ++++
.../schemas/post_actions_connector_execute.ts | 1 +
.../stack_connectors/common/bedrock/schema.ts | 4 +-
.../stack_connectors/common/openai/schema.ts | 12 +++++-
.../connector_types/bedrock/bedrock.test.ts | 4 +-
.../server/connector_types/bedrock/bedrock.ts | 2 +-
.../connector_types/openai/openai.test.ts | 8 +++-
.../server/connector_types/openai/openai.ts | 10 ++++-
.../tests/actions/connector_types/bedrock.ts | 2 +-
17 files changed, 147 insertions(+), 27 deletions(-)
create mode 100644 x-pack/plugins/elastic_assistant/server/lib/executor.ts
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx
index 33dc820f449fa..e8feefbfd2533 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx
@@ -54,7 +54,7 @@ describe('API tests', () => {
expect(mockHttp.fetch).toHaveBeenCalledWith(
'/internal/elastic_assistant/actions/connector/foo/_execute',
{
- body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"}}',
+ body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"},"assistantLangChain":true}',
headers: { 'Content-Type': 'application/json' },
method: 'POST',
signal: undefined,
@@ -72,12 +72,15 @@ describe('API tests', () => {
await fetchConnectorExecuteAction(testProps);
- expect(mockHttp.fetch).toHaveBeenCalledWith('/api/actions/connector/foo/_execute', {
- body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"}}',
- headers: { 'Content-Type': 'application/json' },
- method: 'POST',
- signal: undefined,
- });
+ expect(mockHttp.fetch).toHaveBeenCalledWith(
+ '/internal/elastic_assistant/actions/connector/foo/_execute',
+ {
+ body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"},"assistantLangChain":false}',
+ headers: { 'Content-Type': 'application/json' },
+ method: 'POST',
+ signal: undefined,
+ }
+ );
});
it('returns API_ERROR when the response status is not ok', async () => {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx
index c7c1254656d61..8ccb2e72cfee9 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx
@@ -59,19 +59,16 @@ export const fetchConnectorExecuteAction = async ({
subActionParams: body,
subAction: 'invokeAI',
},
+ assistantLangChain,
};
try {
- const path = assistantLangChain
- ? `/internal/elastic_assistant/actions/connector/${apiConfig?.connectorId}/_execute`
- : `/api/actions/connector/${apiConfig?.connectorId}/_execute`;
-
const response = await http.fetch<{
connector_id: string;
status: string;
data: string;
service_message?: string;
- }>(path, {
+ }>(`/internal/elastic_assistant/actions/connector/${apiConfig?.connectorId}/_execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/action_result_data.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/action_result_data.ts
index 17aa4b83ca67b..dbc095a334cea 100644
--- a/x-pack/plugins/elastic_assistant/server/__mocks__/action_result_data.ts
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/action_result_data.ts
@@ -5,4 +5,7 @@
* 2.0.
*/
-export const mockActionResponse = 'Yes, your name is Andrew. How can I assist you further, Andrew?';
+export const mockActionResponse = {
+ message: 'Yes, your name is Andrew. How can I assist you further, Andrew?',
+ usage: { prompt_tokens: 4, completion_tokens: 10, total_tokens: 14 },
+};
diff --git a/x-pack/plugins/elastic_assistant/server/lib/executor.ts b/x-pack/plugins/elastic_assistant/server/lib/executor.ts
new file mode 100644
index 0000000000000..936e3781731d8
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/lib/executor.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { get } from 'lodash/fp';
+import { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server';
+import { KibanaRequest } from '@kbn/core-http-server';
+import { RequestBody } from './langchain/types';
+
+interface Props {
+ actions: ActionsPluginStart;
+ connectorId: string;
+ request: KibanaRequest;
+}
+interface StaticResponse {
+ connector_id: string;
+ data: string;
+ status: string;
+}
+
+export const executeAction = async ({
+ actions,
+ request,
+ connectorId,
+}: Props): Promise => {
+ const actionsClient = await actions.getActionsClientWithRequest(request);
+ const actionResult = await actionsClient.execute({
+ actionId: connectorId,
+ params: request.body.params,
+ });
+ const content = get('data.message', actionResult);
+ if (typeof content === 'string') {
+ return {
+ connector_id: connectorId,
+ data: content, // the response from the actions framework
+ status: 'ok',
+ };
+ }
+ throw new Error('Unexpected action result');
+};
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.test.ts
index b5f8fa7e88c74..5c27cdef4d3e1 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.test.ts
@@ -51,6 +51,7 @@ const mockRequest: KibanaRequest = {
},
subAction: 'invokeAI',
},
+ assistantLangChain: true,
},
} as KibanaRequest;
@@ -72,7 +73,7 @@ describe('ActionsClientLlm', () => {
await actionsClientLlm._call(prompt); // ignore the result
- expect(actionsClientLlm.getActionResultData()).toEqual(mockActionResponse);
+ expect(actionsClientLlm.getActionResultData()).toEqual(mockActionResponse.message);
});
});
@@ -141,7 +142,7 @@ describe('ActionsClientLlm', () => {
});
it('rejects with the expected error the message has invalid content', async () => {
- const invalidContent = 1234;
+ const invalidContent = { message: 1234 };
mockExecute.mockImplementation(() => ({
data: invalidContent,
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts
index e4403b64d6e0d..f499452e1d764 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts
@@ -92,9 +92,8 @@ export class ActionsClientLlm extends LLM {
`${LLM_TYPE}: action result status is error: ${actionResult?.message} - ${actionResult?.serviceMessage}`
);
}
-
// TODO: handle errors from the connector
- const content = get('data', actionResult);
+ const content = get('data.message', actionResult);
if (typeof content !== 'string') {
throw new Error(
diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts
index b65822524f1cd..1b533e49c4cfe 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts
@@ -105,6 +105,7 @@ export const postEvaluateRoute = (
messages: [],
},
},
+ assistantLangChain: true,
},
};
diff --git a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts
index fa0afb540dc30..507246670833c 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts
@@ -19,6 +19,13 @@ import { coreMock } from '@kbn/core/server/mocks';
jest.mock('../lib/build_response', () => ({
buildResponse: jest.fn().mockImplementation((x) => x),
}));
+jest.mock('../lib/executor', () => ({
+ executeAction: jest.fn().mockImplementation((x) => ({
+ connector_id: 'mock-connector-id',
+ data: mockActionResponse,
+ status: 'ok',
+ })),
+}));
jest.mock('../lib/langchain/execute_custom_llm_chain', () => ({
callAgentExecutor: jest.fn().mockImplementation(
@@ -82,6 +89,7 @@ const mockRequest = {
},
subAction: 'invokeAI',
},
+ assistantLangChain: true,
},
};
@@ -97,7 +105,38 @@ describe('postActionsConnectorExecuteRoute', () => {
jest.clearAllMocks();
});
- it('returns the expected response', async () => {
+ it('returns the expected response when assistantLangChain=false', async () => {
+ const mockRouter = {
+ post: jest.fn().mockImplementation(async (_, handler) => {
+ const result = await handler(
+ mockContext,
+ {
+ ...mockRequest,
+ body: {
+ ...mockRequest.body,
+ assistantLangChain: false,
+ },
+ },
+ mockResponse
+ );
+
+ expect(result).toEqual({
+ body: {
+ connector_id: 'mock-connector-id',
+ data: mockActionResponse,
+ status: 'ok',
+ },
+ });
+ }),
+ };
+
+ await postActionsConnectorExecuteRoute(
+ mockRouter as unknown as IRouter,
+ mockGetElser
+ );
+ });
+
+ it('returns the expected response when assistantLangChain=true', async () => {
const mockRouter = {
post: jest.fn().mockImplementation(async (_, handler) => {
const result = await handler(mockContext, mockRequest, mockResponse);
diff --git a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts
index 5303796d1c983..8da820288ae1b 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts
@@ -7,6 +7,7 @@
import { IRouter, Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
+import { executeAction } from '../lib/executor';
import { POST_ACTIONS_CONNECTOR_EXECUTE } from '../../common/constants';
import { getLangChainMessages } from '../lib/langchain/helpers';
import { buildResponse } from '../lib/build_response';
@@ -41,6 +42,14 @@ export const postActionsConnectorExecuteRoute = (
// get the actions plugin start contract from the request context:
const actions = (await context.elasticAssistant).actions;
+ // if not langchain, call execute action directly and return the response:
+ if (!request.body.assistantLangChain) {
+ const result = await executeAction({ actions, request, connectorId });
+ return response.ok({
+ body: result,
+ });
+ }
+
// get a scoped esClient for assistant memory
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
diff --git a/x-pack/plugins/elastic_assistant/server/schemas/post_actions_connector_execute.ts b/x-pack/plugins/elastic_assistant/server/schemas/post_actions_connector_execute.ts
index b30ccd94e105b..7a8d52e725722 100644
--- a/x-pack/plugins/elastic_assistant/server/schemas/post_actions_connector_execute.ts
+++ b/x-pack/plugins/elastic_assistant/server/schemas/post_actions_connector_execute.ts
@@ -34,6 +34,7 @@ export const PostActionsConnectorExecuteBody = t.type({
]),
subAction: t.string,
}),
+ assistantLangChain: t.boolean,
});
export type PostActionsConnectorExecuteBodyInputs = t.TypeOf<
diff --git a/x-pack/plugins/stack_connectors/common/bedrock/schema.ts b/x-pack/plugins/stack_connectors/common/bedrock/schema.ts
index ac23ed9667ada..64699253c709f 100644
--- a/x-pack/plugins/stack_connectors/common/bedrock/schema.ts
+++ b/x-pack/plugins/stack_connectors/common/bedrock/schema.ts
@@ -34,7 +34,9 @@ export const InvokeAIActionParamsSchema = schema.object({
model: schema.maybe(schema.string()),
});
-export const InvokeAIActionResponseSchema = schema.string();
+export const InvokeAIActionResponseSchema = schema.object({
+ message: schema.string(),
+});
export const RunActionResponseSchema = schema.object(
{
diff --git a/x-pack/plugins/stack_connectors/common/openai/schema.ts b/x-pack/plugins/stack_connectors/common/openai/schema.ts
index fa14aa61fa5b3..fd0b872ab9f36 100644
--- a/x-pack/plugins/stack_connectors/common/openai/schema.ts
+++ b/x-pack/plugins/stack_connectors/common/openai/schema.ts
@@ -44,7 +44,17 @@ export const InvokeAIActionParamsSchema = schema.object({
temperature: schema.maybe(schema.number()),
});
-export const InvokeAIActionResponseSchema = schema.string();
+export const InvokeAIActionResponseSchema = schema.object({
+ message: schema.string(),
+ usage: schema.object(
+ {
+ prompt_tokens: schema.number(),
+ completion_tokens: schema.number(),
+ total_tokens: schema.number(),
+ },
+ { unknowns: 'ignore' }
+ ),
+});
// Execute action schema
export const StreamActionParamsSchema = schema.object({
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts
index 7ee8fd54833c7..dcd3d70f9b4ff 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts
@@ -109,7 +109,7 @@ describe('BedrockConnector', () => {
stop_sequences: ['\n\nHuman:'],
}),
});
- expect(response).toEqual(mockResponseString);
+ expect(response.message).toEqual(mockResponseString);
});
it('Properly formats messages from user, assistant, and system', async () => {
@@ -148,7 +148,7 @@ describe('BedrockConnector', () => {
stop_sequences: ['\n\nHuman:'],
}),
});
- expect(response).toEqual(mockResponseString);
+ expect(response.message).toEqual(mockResponseString);
});
it('errors during API calls are properly handled', async () => {
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts
index 6510731f8ad7e..0e1235312a52c 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts
@@ -150,6 +150,6 @@ export class BedrockConnector extends SubActionConnector {
};
const res = await this.runApi({ body: JSON.stringify(req), model });
- return res.completion.trim();
+ return { message: res.completion.trim() };
}
}
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts
index 00f3b67aafb97..0a4a6a2931d8d 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts
@@ -37,6 +37,11 @@ describe('OpenAIConnector', () => {
index: 0,
},
],
+ usage: {
+ prompt_tokens: 4,
+ completion_tokens: 5,
+ total_tokens: 9,
+ },
},
};
beforeEach(() => {
@@ -273,7 +278,8 @@ describe('OpenAIConnector', () => {
'content-type': 'application/json',
},
});
- expect(response).toEqual(mockResponseString);
+ expect(response.message).toEqual(mockResponseString);
+ expect(response.usage.total_tokens).toEqual(9);
});
it('errors during API calls are properly handled', async () => {
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.ts
index 21c7bc4abdcc0..7413ba56090a1 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.ts
@@ -192,9 +192,15 @@ export class OpenAIConnector extends SubActionConnector {
if (res.choices && res.choices.length > 0 && res.choices[0].message?.content) {
const result = res.choices[0].message.content.trim();
- return result;
+ return { message: result, usage: res.usage };
}
- return 'An error occurred sending your message. \n\nAPI Error: The response from OpenAI was in an unrecognized format.';
+ return {
+ message:
+ 'An error occurred sending your message. \n\nAPI Error: The response from OpenAI was in an unrecognized format.',
+ ...(res.usage
+ ? { usage: res.usage }
+ : { usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } }),
+ };
}
}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/bedrock.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/bedrock.ts
index 4983d19d36b69..67053bef7801b 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/bedrock.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/bedrock.ts
@@ -404,7 +404,7 @@ export default function bedrockTest({ getService }: FtrProviderContext) {
expect(body).to.eql({
status: 'ok',
connector_id: bedrockActionId,
- data: bedrockSuccessResponse.completion,
+ data: { message: bedrockSuccessResponse.completion },
});
});
});
From 726558959f8ab584628047992113f392adf95a54 Mon Sep 17 00:00:00 2001
From: Alexi Doak <109488926+doakalexi@users.noreply.github.com>
Date: Wed, 18 Oct 2023 11:18:20 -0700
Subject: [PATCH 06/14] [ResponseOps][Alerting] Implement and onboard query
delay mechanism for Alerting rules (#168735)
Resolves https://github.com/elastic/kibana/issues/167061
## Summary
This PR will merge the query delay feature branch in to main, and
includes the following PRs:
[[ResponseOps][Alerting] Onboard query delay mechanism for Alerting
rules](https://github.com/elastic/kibana/pull/167363)
[[ResponseOps][Alerting] Implement a query delay mechanism for Alerting
rules](https://github.com/elastic/kibana/pull/167433)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ying Mao
---
.../routes/rules_settings/apis/get/index.ts | 10 +
.../rules_settings/apis/get/types/latest.ts | 8 +
.../rules_settings/apis/get/types/v1.ts | 11 +
.../rules_settings/apis/update/index.ts | 18 +
.../apis/update/schemas/latest.ts | 8 +
.../rules_settings/apis/update/schemas/v1.ts | 12 +
.../apis/update/types/latest.ts | 8 +
.../rules_settings/apis/update/types/v1.ts | 16 +
.../routes/rules_settings/response/index.ts | 12 +
.../rules_settings/response/schemas/latest.ts | 8 +
.../rules_settings/response/schemas/v1.ts | 20 +
.../rules_settings/response/types/latest.ts | 8 +
.../rules_settings/response/types/v1.ts | 11 +
.../plugins/alerting/common/rules_settings.ts | 33 +-
.../server/lib/get_time_range.test.ts | 61 +++
.../alerting/server/lib/get_time_range.ts | 40 ++
x-pack/plugins/alerting/server/plugin.ts | 2 +
.../plugins/alerting/server/routes/index.ts | 4 +
.../apis/get/get_query_delay_settings.test.ts | 72 ++++
.../apis/get/get_query_delay_settings.ts | 39 ++
.../update_query_delay_settings.test.ts | 88 ++++
.../update/update_query_delay_settings.ts | 49 +++
.../routes/rules_settings/transforms/index.ts | 10 +
.../latest.ts | 8 +
.../v1.ts | 23 ++
.../server/rules_settings_client.mock.ts | 14 +-
.../rules_settings_flapping_client.test.ts | 269 +++++++++++-
.../rules_settings_flapping_client.ts | 89 +++-
.../server/rules_settings_client/index.ts | 1 +
.../rules_settings_query_delay_client.test.ts | 385 ++++++++++++++++++
.../rules_settings_query_delay_client.ts | 179 ++++++++
.../rules_settings_client.test.ts | 256 +-----------
.../rules_settings_client.ts | 82 +---
.../schemas/flapping_schema.ts | 14 +
.../rules_settings_client/schemas/index.ts | 9 +
.../schemas/query_delay_schema.ts | 12 +
.../rules_settings_client_factory.test.ts | 4 +
.../server/rules_settings_client_factory.ts | 4 +
.../alerting/server/rules_settings_feature.ts | 39 ++
.../server/task_runner/task_runner.ts | 4 +
x-pack/plugins/alerting/server/types.ts | 8 +-
.../register_anomaly_rule_type.test.ts | 6 +-
.../anomaly/register_anomaly_rule_type.ts | 22 +-
.../register_error_count_rule_type.ts | 7 +-
...register_transaction_duration_rule_type.ts | 13 +-
...gister_transaction_error_rate_rule_type.ts | 7 +-
.../server/routes/alerts/test_utils/index.ts | 4 +
...nventory_metric_threshold_executor.test.ts | 4 +
.../metric_threshold_executor.test.ts | 4 +
.../custom_threshold_executor.test.ts | 4 +
.../custom_threshold_executor.ts | 5 +-
.../lib/create_timerange.test.ts | 170 +++-----
.../custom_threshold/lib/create_timerange.ts | 19 +-
.../custom_threshold/lib/evaluate_rule.ts | 2 +-
.../lib/rules/slo_burn_rate/executor.test.ts | 11 +
.../lib/rules/slo_burn_rate/executor.ts | 33 +-
.../slo_burn_rate/lib/build_query.test.ts | 17 +-
.../rules/slo_burn_rate/lib/build_query.ts | 43 +-
.../lib/rules/slo_burn_rate/lib/evaluate.ts | 19 +-
.../utils/create_lifecycle_rule_type.test.ts | 4 +
.../utils/rule_executor.test_helpers.ts | 4 +
...gacy_rules_notification_alert_type.test.ts | 4 +
.../rule_preview/api/preview_rules/route.ts | 4 +
.../rule_types/es_query/executor.test.ts | 69 +---
.../server/rule_types/es_query/executor.ts | 60 +--
.../es_query/lib/fetch_es_query.test.ts | 37 +-
.../rule_types/es_query/lib/fetch_es_query.ts | 17 +-
.../es_query/lib/fetch_esql_query.test.ts | 32 +-
.../es_query/lib/fetch_esql_query.ts | 34 +-
.../lib/fetch_search_source_query.test.ts | 45 +-
.../es_query/lib/fetch_search_source_query.ts | 36 +-
.../es_query/lib/get_search_params.ts | 56 ---
.../rule_types/es_query/rule_type.test.ts | 107 ++---
.../server/rule_types/es_query/util.test.ts | 51 +++
.../server/rule_types/es_query/util.ts | 28 ++
.../index_threshold/rule_type.test.ts | 9 +
.../rule_types/index_threshold/rule_type.ts | 13 +-
.../translations/translations/fr-FR.json | 2 -
.../translations/translations/ja-JP.json | 2 -
.../translations/translations/zh-CN.json | 2 -
.../rules_settings_flapping_form_section.tsx | 73 +---
.../rules_settings_flapping_section.tsx | 185 +++++++++
.../rules_settings_query_delay_section.tsx | 123 ++++++
.../rules_settings_link.stories.tsx | 6 +
.../rules_settings_link.test.tsx | 79 +++-
.../rules_setting/rules_settings_link.tsx | 4 +-
.../rules_settings_modal.test.tsx | 164 +++++++-
.../rules_setting/rules_settings_modal.tsx | 249 +++++------
.../rules_setting/rules_settings_range.tsx | 51 +++
.../hooks/use_get_flapping_settings.ts | 15 +-
.../use_get_query_delay_setting.test.tsx | 57 +++
.../hooks/use_get_query_delay_settings.ts | 40 ++
.../hooks/use_update_rules_settings.test.tsx | 109 +++++
...ttings.ts => use_update_rules_settings.ts} | 20 +-
.../rule_api/get_query_delay_settings.test.ts | 30 ++
.../lib/rule_api/get_query_delay_settings.ts | 22 +
.../update_query_delay_settings.test.ts | 33 ++
.../rule_api/update_query_delay_settings.ts | 44 ++
.../server/data/lib/time_series_query.test.ts | 22 +
.../server/data/lib/time_series_query.ts | 13 +-
.../common/lib/reset_rules_settings.ts | 17 +-
.../alerting/get_query_delay_settings.ts | 65 +++
.../group3/tests/alerting/index.ts | 2 +
.../alerting/update_query_delay_settings.ts | 103 +++++
.../apis/security/privileges.ts | 2 +
.../apis/security/privileges_basic.ts | 2 +
.../triggers_actions_ui/rules_settings.ts | 30 +-
107 files changed, 3394 insertions(+), 1120 deletions(-)
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/get/index.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/latest.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/v1.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/update/index.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/latest.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/v1.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/latest.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/v1.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/response/index.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/latest.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/v1.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/response/types/latest.ts
create mode 100644 x-pack/plugins/alerting/common/routes/rules_settings/response/types/v1.ts
create mode 100644 x-pack/plugins/alerting/server/lib/get_time_range.test.ts
create mode 100644 x-pack/plugins/alerting/server/lib/get_time_range.ts
create mode 100644 x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts
create mode 100644 x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.ts
create mode 100644 x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts
create mode 100644 x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.ts
create mode 100644 x-pack/plugins/alerting/server/routes/rules_settings/transforms/index.ts
create mode 100644 x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/latest.ts
create mode 100644 x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/v1.ts
create mode 100644 x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts
create mode 100644 x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts
create mode 100644 x-pack/plugins/alerting/server/rules_settings_client/schemas/flapping_schema.ts
create mode 100644 x-pack/plugins/alerting/server/rules_settings_client/schemas/index.ts
create mode 100644 x-pack/plugins/alerting/server/rules_settings_client/schemas/query_delay_schema.ts
delete mode 100644 x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/get_search_params.ts
create mode 100644 x-pack/plugins/stack_alerts/server/rule_types/es_query/util.test.ts
rename x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/{ => flapping}/rules_settings_flapping_form_section.tsx (77%)
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_section.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/query_delay/rules_settings_query_delay_section.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_range.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_settings.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx
rename x-pack/plugins/triggers_actions_ui/public/application/hooks/{use_update_flapping_settings.ts => use_update_rules_settings.ts} (68%)
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.test.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.test.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.ts
create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_query_delay_settings.ts
create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_query_delay_settings.ts
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/index.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/index.ts
new file mode 100644
index 0000000000000..677bf4c52d5ec
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export type { GetQueryDelaySettingsResponse } from './types/latest';
+
+export type { GetQueryDelaySettingsResponse as GetQueryDelaySettingsResponseV1 } from './types/v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/latest.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/latest.ts
new file mode 100644
index 0000000000000..4cf7e8676c7a7
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export type { GetQueryDelaySettingsResponse } from './v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/v1.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/v1.ts
new file mode 100644
index 0000000000000..040f3c4813478
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/get/types/v1.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 type { TypeOf } from '@kbn/config-schema';
+import { queryDelaySettingsResponseSchemaV1 } from '../../../response';
+
+export type GetQueryDelaySettingsResponse = TypeOf;
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/index.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/index.ts
new file mode 100644
index 0000000000000..274f279dcd981
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/index.ts
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+export { updateQueryDelaySettingsBodySchema } from './schemas/latest';
+export type {
+ UpdateQueryDelaySettingsRequestBody,
+ UpdateQueryDelaySettingsResponse,
+} from './types/latest';
+
+export { updateQueryDelaySettingsBodySchema as updateQueryDelaySettingsBodySchemaV1 } from './schemas/v1';
+export type {
+ UpdateQueryDelaySettingsRequestBody as UpdateQueryDelaySettingsRequestBodyV1,
+ UpdateQueryDelaySettingsResponse as UpdateQueryDelaySettingsResponseV1,
+} from './types/v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/latest.ts
new file mode 100644
index 0000000000000..25300c97a6d2e
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/v1.ts
new file mode 100644
index 0000000000000..8e1865b77c273
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/schemas/v1.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+
+export const updateQueryDelaySettingsBodySchema = schema.object({
+ delay: schema.number(),
+});
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/latest.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/latest.ts
new file mode 100644
index 0000000000000..25300c97a6d2e
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/v1.ts b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/v1.ts
new file mode 100644
index 0000000000000..0b421e73150f5
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/apis/update/types/v1.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 type { TypeOf } from '@kbn/config-schema';
+import { queryDelaySettingsResponseSchemaV1 } from '../../../response';
+import { updateQueryDelaySettingsBodySchemaV1 } from '..';
+
+export type UpdateQueryDelaySettingsRequestBody = TypeOf<
+ typeof updateQueryDelaySettingsBodySchemaV1
+>;
+
+export type UpdateQueryDelaySettingsResponse = TypeOf;
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/response/index.ts b/x-pack/plugins/alerting/common/routes/rules_settings/response/index.ts
new file mode 100644
index 0000000000000..f0a0070f37e74
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/response/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+export { queryDelaySettingsResponseSchema } from './schemas/latest';
+export type { QueryDelaySettingsResponse } from './types/latest';
+
+export { queryDelaySettingsResponseSchema as queryDelaySettingsResponseSchemaV1 } from './schemas/v1';
+export type { QueryDelaySettingsResponse as QueryDelaySettingsResponseV1 } from './types/v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/latest.ts
new file mode 100644
index 0000000000000..25300c97a6d2e
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/v1.ts
new file mode 100644
index 0000000000000..59676b865c601
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/response/schemas/v1.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+
+export const queryDelaySettingsResponseBodySchema = schema.object({
+ delay: schema.number(),
+ created_by: schema.nullable(schema.string()),
+ updated_by: schema.nullable(schema.string()),
+ created_at: schema.string(),
+ updated_at: schema.string(),
+});
+
+export const queryDelaySettingsResponseSchema = schema.object({
+ body: queryDelaySettingsResponseBodySchema,
+});
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/response/types/latest.ts b/x-pack/plugins/alerting/common/routes/rules_settings/response/types/latest.ts
new file mode 100644
index 0000000000000..25300c97a6d2e
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/response/types/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './v1';
diff --git a/x-pack/plugins/alerting/common/routes/rules_settings/response/types/v1.ts b/x-pack/plugins/alerting/common/routes/rules_settings/response/types/v1.ts
new file mode 100644
index 0000000000000..b5671b2d54628
--- /dev/null
+++ b/x-pack/plugins/alerting/common/routes/rules_settings/response/types/v1.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 type { TypeOf } from '@kbn/config-schema';
+import { queryDelaySettingsResponseSchemaV1 } from '..';
+
+export type QueryDelaySettingsResponse = TypeOf;
diff --git a/x-pack/plugins/alerting/common/rules_settings.ts b/x-pack/plugins/alerting/common/rules_settings.ts
index 743d5f4236aaa..953f29144a100 100644
--- a/x-pack/plugins/alerting/common/rules_settings.ts
+++ b/x-pack/plugins/alerting/common/rules_settings.ts
@@ -20,29 +20,51 @@ export interface RulesSettingsFlappingProperties {
export type RulesSettingsFlapping = RulesSettingsFlappingProperties &
RulesSettingsModificationMetadata;
+export interface RulesSettingsQueryDelayProperties {
+ delay: number;
+}
+
+export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties &
+ RulesSettingsModificationMetadata;
+
+export interface RulesSettingsProperties {
+ flapping?: RulesSettingsFlappingProperties;
+ queryDelay?: RulesSettingsQueryDelayProperties;
+}
+
export interface RulesSettings {
- flapping: RulesSettingsFlapping;
+ flapping?: RulesSettingsFlapping;
+ queryDelay?: RulesSettingsQueryDelay;
}
export const MIN_LOOK_BACK_WINDOW = 2;
export const MAX_LOOK_BACK_WINDOW = 20;
export const MIN_STATUS_CHANGE_THRESHOLD = 2;
export const MAX_STATUS_CHANGE_THRESHOLD = 20;
+export const MIN_QUERY_DELAY = 0;
+export const MAX_QUERY_DELAY = 60;
export const RULES_SETTINGS_FEATURE_ID = 'rulesSettings';
export const ALL_FLAPPING_SETTINGS_SUB_FEATURE_ID = 'allFlappingSettings';
export const READ_FLAPPING_SETTINGS_SUB_FEATURE_ID = 'readFlappingSettings';
+export const ALL_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID = 'allQueryDelaySettings';
+export const READ_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID = 'readQueryDelaySettings';
export const API_PRIVILEGES = {
READ_FLAPPING_SETTINGS: 'read-flapping-settings',
WRITE_FLAPPING_SETTINGS: 'write-flapping-settings',
+ READ_QUERY_DELAY_SETTINGS: 'read-query-delay-settings',
+ WRITE_QUERY_DELAY_SETTINGS: 'write-query-delay-settings',
};
export const RULES_SETTINGS_SAVED_OBJECT_TYPE = 'rules-settings';
-export const RULES_SETTINGS_SAVED_OBJECT_ID = 'rules-settings';
+export const RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID = 'rules-settings';
+export const RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID = 'query-delay-settings';
export const DEFAULT_LOOK_BACK_WINDOW = 20;
export const DEFAULT_STATUS_CHANGE_THRESHOLD = 4;
+export const DEFAULT_QUERY_DELAY = 0;
+export const DEFAULT_SERVERLESS_QUERY_DELAY = 15;
export const DEFAULT_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = {
enabled: true,
@@ -54,3 +76,10 @@ export const DISABLE_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = {
...DEFAULT_FLAPPING_SETTINGS,
enabled: false,
};
+
+export const DEFAULT_QUERY_DELAY_SETTINGS: RulesSettingsQueryDelayProperties = {
+ delay: DEFAULT_QUERY_DELAY,
+};
+export const DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS: RulesSettingsQueryDelayProperties = {
+ delay: DEFAULT_SERVERLESS_QUERY_DELAY,
+};
diff --git a/x-pack/plugins/alerting/server/lib/get_time_range.test.ts b/x-pack/plugins/alerting/server/lib/get_time_range.test.ts
new file mode 100644
index 0000000000000..684aea523e3ba
--- /dev/null
+++ b/x-pack/plugins/alerting/server/lib/get_time_range.test.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { loggingSystemMock } from '@kbn/core/server/mocks';
+import { getTimeRange } from './get_time_range';
+
+describe('getTimeRange', () => {
+ const logger = loggingSystemMock.create().get();
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2023-10-04T00:00:00.000Z'));
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ test('returns time range with no query delay', () => {
+ const { dateStart, dateEnd } = getTimeRange(logger, { delay: 0 }, '5m');
+ expect(dateStart).toBe('2023-10-03T23:55:00.000Z');
+ expect(dateEnd).toBe('2023-10-04T00:00:00.000Z');
+ expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 0 seconds');
+ });
+
+ test('returns time range with a query delay', () => {
+ const { dateStart, dateEnd } = getTimeRange(logger, { delay: 45 }, '5m');
+ expect(dateStart).toBe('2023-10-03T23:54:15.000Z');
+ expect(dateEnd).toBe('2023-10-03T23:59:15.000Z');
+ expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 45 seconds');
+ });
+
+ test('returns time range with no query delay and no time range', () => {
+ const { dateStart, dateEnd } = getTimeRange(logger, { delay: 0 });
+ expect(dateStart).toBe('2023-10-04T00:00:00.000Z');
+ expect(dateEnd).toBe('2023-10-04T00:00:00.000Z');
+ expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 0 seconds');
+ });
+
+ test('returns time range with a query delay and no time range', () => {
+ const { dateStart, dateEnd } = getTimeRange(logger, { delay: 45 });
+ expect(dateStart).toBe('2023-10-03T23:59:15.000Z');
+ expect(dateEnd).toBe('2023-10-03T23:59:15.000Z');
+ expect(logger.debug).toHaveBeenCalledWith('Adjusting rule query time range by 45 seconds');
+ });
+
+ test('throws an error when the time window is invalid', () => {
+ expect(() => getTimeRange(logger, { delay: 45 }, '5k')).toThrowErrorMatchingInlineSnapshot(
+ `"Invalid format for windowSize: \\"5k\\""`
+ );
+ expect(logger.debug).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/alerting/server/lib/get_time_range.ts b/x-pack/plugins/alerting/server/lib/get_time_range.ts
new file mode 100644
index 0000000000000..001b5df614ddd
--- /dev/null
+++ b/x-pack/plugins/alerting/server/lib/get_time_range.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { Logger } from '@kbn/logging';
+import { parseDuration, RulesSettingsQueryDelayProperties } from '../../common';
+
+export function getTimeRange(
+ logger: Logger,
+ queryDelaySettings: RulesSettingsQueryDelayProperties,
+ window?: string
+) {
+ let timeWindow: number = 0;
+ if (window) {
+ try {
+ timeWindow = parseDuration(window);
+ } catch (err) {
+ throw new Error(
+ i18n.translate('xpack.alerting.invalidWindowSizeErrorMessage', {
+ defaultMessage: 'Invalid format for windowSize: "{window}"',
+ values: {
+ window,
+ },
+ })
+ );
+ }
+ }
+ logger.debug(`Adjusting rule query time range by ${queryDelaySettings.delay} seconds`);
+
+ const queryDelay = queryDelaySettings.delay * 1000;
+ const date = Date.now();
+ const dateStart = new Date(date - (timeWindow + queryDelay)).toISOString();
+ const dateEnd = new Date(date - queryDelay).toISOString();
+
+ return { dateStart, dateEnd };
+}
diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts
index e9590f883cc53..bb950973afae7 100644
--- a/x-pack/plugins/alerting/server/plugin.ts
+++ b/x-pack/plugins/alerting/server/plugin.ts
@@ -190,6 +190,7 @@ export interface AlertingPluginsStart {
data: DataPluginStart;
dataViews: DataViewsPluginStart;
share: SharePluginStart;
+ serverless?: ServerlessPluginSetup;
}
export class AlertingPlugin {
@@ -503,6 +504,7 @@ export class AlertingPlugin {
logger: this.logger,
savedObjectsService: core.savedObjects,
securityPluginStart: plugins.security,
+ isServerless: !!plugins.serverless,
});
maintenanceWindowClientFactory.initialize({
diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts
index a90ad5feba4da..93c66c45ce2af 100644
--- a/x-pack/plugins/alerting/server/routes/index.ts
+++ b/x-pack/plugins/alerting/server/routes/index.ts
@@ -62,6 +62,8 @@ import { registerRulesValueSuggestionsRoute } from './suggestions/values_suggest
import { registerFieldsRoute } from './suggestions/fields_rules';
import { bulkGetMaintenanceWindowRoute } from './maintenance_window/apis/bulk_get/bulk_get_maintenance_windows_route';
import { registerAlertsValueSuggestionsRoute } from './suggestions/values_suggestion_alerts';
+import { getQueryDelaySettingsRoute } from './rules_settings/apis/get/get_query_delay_settings';
+import { updateQueryDelaySettingsRoute } from './rules_settings/apis/update/update_query_delay_settings';
export interface RouteOptions {
router: IRouter;
@@ -133,4 +135,6 @@ export function defineRoutes(opts: RouteOptions) {
bulkGetMaintenanceWindowRoute(router, licenseState);
getScheduleFrequencyRoute(router, licenseState);
bulkUntrackAlertRoute(router, licenseState);
+ getQueryDelaySettingsRoute(router, licenseState);
+ updateQueryDelaySettingsRoute(router, licenseState);
}
diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts b/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts
new file mode 100644
index 0000000000000..4102aa80b29af
--- /dev/null
+++ b/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.test.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { httpServiceMock } from '@kbn/core/server/mocks';
+import { licenseStateMock } from '../../../../lib/license_state.mock';
+import { mockHandlerArguments } from '../../../_mock_handler_arguments';
+import {
+ rulesSettingsClientMock,
+ RulesSettingsClientMock,
+} from '../../../../rules_settings_client.mock';
+import { getQueryDelaySettingsRoute } from './get_query_delay_settings';
+
+let rulesSettingsClient: RulesSettingsClientMock;
+
+jest.mock('../../../../lib/license_api_access', () => ({
+ verifyApiAccess: jest.fn(),
+}));
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ rulesSettingsClient = rulesSettingsClientMock.create();
+});
+
+describe('getQueryDelaySettingsRoute', () => {
+ test('gets query delay settings', async () => {
+ const licenseState = licenseStateMock.create();
+ const router = httpServiceMock.createRouter();
+
+ getQueryDelaySettingsRoute(router, licenseState);
+
+ const [config, handler] = router.get.mock.calls[0];
+
+ expect(config).toMatchInlineSnapshot(`
+ Object {
+ "options": Object {
+ "tags": Array [
+ "access:read-query-delay-settings",
+ ],
+ },
+ "path": "/internal/alerting/rules/settings/_query_delay",
+ "validate": Object {},
+ }
+ `);
+
+ (rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValue({
+ delay: 10,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ });
+
+ const [context, req, res] = mockHandlerArguments({ rulesSettingsClient }, {}, ['ok']);
+
+ await handler(context, req, res);
+
+ expect(rulesSettingsClient.queryDelay().get).toHaveBeenCalledTimes(1);
+ expect(res.ok).toHaveBeenCalledWith({
+ body: expect.objectContaining({
+ delay: 10,
+ created_by: 'test name',
+ updated_by: 'test name',
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ }),
+ });
+ });
+});
diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.ts b/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.ts
new file mode 100644
index 0000000000000..ee16be9642977
--- /dev/null
+++ b/x-pack/plugins/alerting/server/routes/rules_settings/apis/get/get_query_delay_settings.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { IRouter } from '@kbn/core/server';
+import { ILicenseState } from '../../../../lib';
+import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
+import { verifyAccessAndContext } from '../../../lib';
+import { API_PRIVILEGES } from '../../../../../common';
+import { transformQueryDelaySettingsToResponseV1 } from '../../transforms';
+import { GetQueryDelaySettingsResponseV1 } from '../../../../../common/routes/rules_settings/apis/get';
+
+export const getQueryDelaySettingsRoute = (
+ router: IRouter,
+ licenseState: ILicenseState
+) => {
+ router.get(
+ {
+ path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`,
+ validate: {},
+ options: {
+ tags: [`access:${API_PRIVILEGES.READ_QUERY_DELAY_SETTINGS}`],
+ },
+ },
+ router.handleLegacyErrors(
+ verifyAccessAndContext(licenseState, async function (context, req, res) {
+ const rulesSettingsClient = (await context.alerting).getRulesSettingsClient();
+ const queryDelaySettings = await rulesSettingsClient.queryDelay().get();
+ const response: GetQueryDelaySettingsResponseV1 =
+ transformQueryDelaySettingsToResponseV1(queryDelaySettings);
+
+ return res.ok(response);
+ })
+ )
+ );
+};
diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts b/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts
new file mode 100644
index 0000000000000..8a506809131ab
--- /dev/null
+++ b/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.test.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 { httpServiceMock } from '@kbn/core/server/mocks';
+import { licenseStateMock } from '../../../../lib/license_state.mock';
+import { mockHandlerArguments } from '../../../_mock_handler_arguments';
+import {
+ rulesSettingsClientMock,
+ RulesSettingsClientMock,
+} from '../../../../rules_settings_client.mock';
+import { updateQueryDelaySettingsRoute } from './update_query_delay_settings';
+
+let rulesSettingsClient: RulesSettingsClientMock;
+
+jest.mock('../../../../lib/license_api_access', () => ({
+ verifyApiAccess: jest.fn(),
+}));
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ rulesSettingsClient = rulesSettingsClientMock.create();
+});
+
+const mockQueryDelaySettings = {
+ delay: 10,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+};
+
+describe('updateQueryDelaySettingsRoute', () => {
+ test('updates query delay settings', async () => {
+ const licenseState = licenseStateMock.create();
+ const router = httpServiceMock.createRouter();
+
+ updateQueryDelaySettingsRoute(router, licenseState);
+
+ const [config, handler] = router.post.mock.calls[0];
+
+ expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rules/settings/_query_delay"`);
+ expect(config.options).toMatchInlineSnapshot(`
+ Object {
+ "tags": Array [
+ "access:write-query-delay-settings",
+ ],
+ }
+ `);
+
+ (rulesSettingsClient.queryDelay().get as jest.Mock).mockResolvedValue(mockQueryDelaySettings);
+ (rulesSettingsClient.queryDelay().update as jest.Mock).mockResolvedValue(
+ mockQueryDelaySettings
+ );
+
+ const updateResult = {
+ delay: 6,
+ };
+
+ const [context, req, res] = mockHandlerArguments(
+ { rulesSettingsClient },
+ {
+ body: updateResult,
+ },
+ ['ok']
+ );
+
+ await handler(context, req, res);
+
+ expect(rulesSettingsClient.queryDelay().update).toHaveBeenCalledTimes(1);
+ expect((rulesSettingsClient.queryDelay().update as jest.Mock).mock.calls[0])
+ .toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "delay": 6,
+ },
+ ]
+ `);
+ expect(res.ok).toHaveBeenCalledWith({
+ body: expect.objectContaining({
+ delay: 10,
+ }),
+ });
+ });
+});
diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.ts b/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.ts
new file mode 100644
index 0000000000000..050f28942fda7
--- /dev/null
+++ b/x-pack/plugins/alerting/server/routes/rules_settings/apis/update/update_query_delay_settings.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { IRouter } from '@kbn/core/server';
+import { ILicenseState } from '../../../../lib';
+import { verifyAccessAndContext } from '../../../lib';
+import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
+import { API_PRIVILEGES } from '../../../../../common';
+import {
+ updateQueryDelaySettingsBodySchemaV1,
+ UpdateQueryDelaySettingsRequestBodyV1,
+ UpdateQueryDelaySettingsResponseV1,
+} from '../../../../../common/routes/rules_settings/apis/update';
+import { transformQueryDelaySettingsToResponseV1 } from '../../transforms';
+
+export const updateQueryDelaySettingsRoute = (
+ router: IRouter,
+ licenseState: ILicenseState
+) => {
+ router.post(
+ {
+ path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`,
+ validate: {
+ body: updateQueryDelaySettingsBodySchemaV1,
+ },
+ options: {
+ tags: [`access:${API_PRIVILEGES.WRITE_QUERY_DELAY_SETTINGS}`],
+ },
+ },
+ router.handleLegacyErrors(
+ verifyAccessAndContext(licenseState, async function (context, req, res) {
+ const rulesSettingsClient = (await context.alerting).getRulesSettingsClient();
+
+ const body: UpdateQueryDelaySettingsRequestBodyV1 = req.body;
+
+ const updatedQueryDelaySettings = await rulesSettingsClient.queryDelay().update(body);
+
+ const response: UpdateQueryDelaySettingsResponseV1 =
+ transformQueryDelaySettingsToResponseV1(updatedQueryDelaySettings);
+
+ return res.ok(response);
+ })
+ )
+ );
+};
diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/transforms/index.ts b/x-pack/plugins/alerting/server/routes/rules_settings/transforms/index.ts
new file mode 100644
index 0000000000000..5a7438d7f3ad9
--- /dev/null
+++ b/x-pack/plugins/alerting/server/routes/rules_settings/transforms/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export { transformQueryDelaySettingsToResponse } from './transform_query_delay_settings_to_response/latest';
+
+export { transformQueryDelaySettingsToResponse as transformQueryDelaySettingsToResponseV1 } from './transform_query_delay_settings_to_response/v1';
diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/latest.ts b/x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/latest.ts
new file mode 100644
index 0000000000000..25300c97a6d2e
--- /dev/null
+++ b/x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './v1';
diff --git a/x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/v1.ts b/x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/v1.ts
new file mode 100644
index 0000000000000..926b702bdbf9c
--- /dev/null
+++ b/x-pack/plugins/alerting/server/routes/rules_settings/transforms/transform_query_delay_settings_to_response/v1.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 { RulesSettingsQueryDelay } from '../../../../../common';
+import { QueryDelaySettingsResponseV1 } from '../../../../../common/routes/rules_settings/response';
+
+export const transformQueryDelaySettingsToResponse = (
+ settings: RulesSettingsQueryDelay
+): QueryDelaySettingsResponseV1 => {
+ return {
+ body: {
+ delay: settings.delay,
+ created_by: settings.createdBy,
+ updated_by: settings.updatedBy,
+ created_at: settings.createdAt,
+ updated_at: settings.updatedAt,
+ },
+ };
+};
diff --git a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts
index 99dcfc388ca23..12703161fdb46 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts
@@ -8,11 +8,14 @@
import {
RulesSettingsClientApi,
RulesSettingsFlappingClientApi,
+ RulesSettingsQueryDelayClientApi,
DEFAULT_FLAPPING_SETTINGS,
+ DEFAULT_QUERY_DELAY_SETTINGS,
} from './types';
export type RulesSettingsClientMock = jest.Mocked;
export type RulesSettingsFlappingClientMock = jest.Mocked;
+export type RulesSettingsQueryDelayClientMock = jest.Mocked;
// Warning: Becareful when resetting all mocks in tests as it would clear
// the mock return value on the flapping
@@ -20,11 +23,18 @@ const createRulesSettingsClientMock = () => {
const flappingMocked: RulesSettingsFlappingClientMock = {
get: jest.fn().mockReturnValue(DEFAULT_FLAPPING_SETTINGS),
update: jest.fn(),
+ getSettings: jest.fn(),
+ createSettings: jest.fn(),
+ };
+ const queryDelayMocked: RulesSettingsQueryDelayClientMock = {
+ get: jest.fn().mockReturnValue(DEFAULT_QUERY_DELAY_SETTINGS),
+ update: jest.fn(),
+ getSettings: jest.fn(),
+ createSettings: jest.fn(),
};
const mocked: RulesSettingsClientMock = {
- get: jest.fn(),
- create: jest.fn(),
flapping: jest.fn().mockReturnValue(flappingMocked),
+ queryDelay: jest.fn().mockReturnValue(queryDelayMocked),
};
return mocked;
};
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts
index 2b02af1327eea..19f978a8985f1 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts
@@ -13,10 +13,11 @@ import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mock
import {
RULES_SETTINGS_FEATURE_ID,
RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
DEFAULT_FLAPPING_SETTINGS,
RulesSettings,
} from '../../../common';
+import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
const mockDateString = '2019-02-12T21:01:22.479Z';
@@ -39,13 +40,6 @@ const getMockRulesSettings = (): RulesSettings => {
const rulesSettingsFlappingClientParams: jest.Mocked =
{
logger: loggingSystemMock.create().get(),
- getOrCreate: jest.fn().mockReturnValue({
- id: RULES_SETTINGS_FEATURE_ID,
- type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
- attributes: getMockRulesSettings(),
- references: [],
- version: '123',
- }),
getModificationMetadata: jest.fn(),
savedObjectsClient,
};
@@ -58,9 +52,21 @@ const updatedMetadata = {
};
describe('RulesSettingsFlappingClient', () => {
- beforeEach(() =>
- rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValue(updatedMetadata)
- );
+ beforeEach(() => {
+ rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValue(updatedMetadata);
+ savedObjectsClient.get.mockResolvedValue({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: getMockRulesSettings(),
+ references: [],
+ version: '123',
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date(mockDateString));
@@ -119,7 +125,7 @@ describe('RulesSettingsFlappingClient', () => {
expect(savedObjectsClient.update).toHaveBeenCalledWith(
RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
{
flapping: expect.objectContaining({
enabled: false,
@@ -192,4 +198,243 @@ describe('RulesSettingsFlappingClient', () => {
'Invalid values,lookBackWindow (10) must be equal to or greater than statusChangeThreshold (20).'
);
});
+
+ test('can create a new flapping settings saved object', async () => {
+ rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+
+ const result = await client.createSettings();
+
+ expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ flapping: expect.objectContaining({
+ enabled: mockAttributes.flapping?.enabled,
+ lookBackWindow: mockAttributes.flapping?.lookBackWindow,
+ statusChangeThreshold: mockAttributes.flapping?.statusChangeThreshold,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ {
+ id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+ expect(result.attributes).toEqual(mockAttributes);
+ });
+
+ test('can get existing flapping settings saved object', async () => {
+ const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+ const result = await client.getSettings();
+ expect(result.attributes).toEqual(mockAttributes);
+ });
+
+ test('throws if there is no existing saved object to get', async () => {
+ const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
+
+ savedObjectsClient.get.mockRejectedValueOnce(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
+ )
+ );
+ await expect(client.getSettings()).rejects.toThrowError();
+ });
+
+ test('can persist flapping settings when saved object does not exist', async () => {
+ rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
+ const mockAttributes = getMockRulesSettings();
+ savedObjectsClient.get.mockRejectedValueOnce(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
+ )
+ );
+
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+
+ const result = await client.get();
+
+ expect(savedObjectsClient.get).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
+ );
+
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ flapping: expect.objectContaining({
+ enabled: mockAttributes.flapping?.enabled,
+ lookBackWindow: mockAttributes.flapping?.lookBackWindow,
+ statusChangeThreshold: mockAttributes.flapping?.statusChangeThreshold,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ {
+ id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+ expect(result).toEqual(mockAttributes.flapping);
+ });
+
+ test('can persist flapping settings when saved object already exists', async () => {
+ rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+
+ const result = await client.get();
+
+ expect(savedObjectsClient.get).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
+ );
+ expect(savedObjectsClient.create).not.toHaveBeenCalled();
+ expect(result).toEqual(mockAttributes.flapping);
+ });
+
+ test('can update flapping settings when saved object does not exist', async () => {
+ rulesSettingsFlappingClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsFlappingClient(rulesSettingsFlappingClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.get.mockRejectedValueOnce(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
+ )
+ );
+
+ const mockResolve = {
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ version: '123',
+ };
+
+ savedObjectsClient.create.mockResolvedValueOnce(mockResolve);
+ savedObjectsClient.update.mockResolvedValueOnce({
+ ...mockResolve,
+ attributes: {
+ flapping: {
+ ...mockResolve.attributes.flapping,
+ enabled: false,
+ lookBackWindow: 5,
+ statusChangeThreshold: 5,
+ },
+ },
+ });
+
+ // Try to update with new values
+ const result = await client.update({
+ enabled: false,
+ lookBackWindow: 5,
+ statusChangeThreshold: 5,
+ });
+
+ // Tried to get first, but no results
+ expect(savedObjectsClient.get).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
+ );
+
+ // So create a new entry
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ flapping: expect.objectContaining({
+ enabled: mockAttributes.flapping?.enabled,
+ lookBackWindow: mockAttributes.flapping?.lookBackWindow,
+ statusChangeThreshold: mockAttributes.flapping?.statusChangeThreshold,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ {
+ id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+
+ // Try to update with version
+ expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
+ {
+ flapping: expect.objectContaining({
+ enabled: false,
+ lookBackWindow: 5,
+ statusChangeThreshold: 5,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ { version: '123' }
+ );
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ enabled: false,
+ lookBackWindow: 5,
+ statusChangeThreshold: 5,
+ })
+ );
+ });
});
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts
index 88052ea8cfb6e..0bf6f2af025fe 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts
@@ -6,7 +6,12 @@
*/
import Boom from '@hapi/boom';
-import { Logger, SavedObjectsClientContract, SavedObject } from '@kbn/core/server';
+import {
+ Logger,
+ SavedObjectsClientContract,
+ SavedObject,
+ SavedObjectsErrorHelpers,
+} from '@kbn/core/server';
import {
RulesSettings,
RulesSettingsFlapping,
@@ -17,8 +22,11 @@ import {
MIN_STATUS_CHANGE_THRESHOLD,
MAX_STATUS_CHANGE_THRESHOLD,
RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
+ DEFAULT_FLAPPING_SETTINGS,
} from '../../../common';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { flappingSchema } from '../schemas';
const verifyFlappingSettings = (flappingSettings: RulesSettingsFlappingProperties) => {
const { lookBackWindow, statusChangeThreshold } = flappingSettings;
@@ -48,30 +56,42 @@ const verifyFlappingSettings = (flappingSettings: RulesSettingsFlappingPropertie
export interface RulesSettingsFlappingClientConstructorOptions {
readonly logger: Logger;
readonly savedObjectsClient: SavedObjectsClientContract;
- readonly getOrCreate: () => Promise>;
readonly getModificationMetadata: () => Promise;
}
export class RulesSettingsFlappingClient {
private readonly logger: Logger;
private readonly savedObjectsClient: SavedObjectsClientContract;
- private readonly getOrCreate: () => Promise>;
private readonly getModificationMetadata: () => Promise;
constructor(options: RulesSettingsFlappingClientConstructorOptions) {
this.logger = options.logger;
this.savedObjectsClient = options.savedObjectsClient;
- this.getOrCreate = options.getOrCreate;
this.getModificationMetadata = options.getModificationMetadata;
}
public async get(): Promise {
const rulesSettings = await this.getOrCreate();
+ if (!rulesSettings.attributes.flapping) {
+ this.logger.error('Failed to get flapping rules setting for current space.');
+ throw new Error(
+ 'Failed to get flapping rules setting for current space. Flapping settings are undefined'
+ );
+ }
return rulesSettings.attributes.flapping;
}
public async update(newFlappingProperties: RulesSettingsFlappingProperties) {
+ return await retryIfConflicts(
+ this.logger,
+ 'ruleSettingsClient.flapping.update()',
+ async () => await this.updateWithOCC(newFlappingProperties)
+ );
+ }
+
+ private async updateWithOCC(newFlappingProperties: RulesSettingsFlappingProperties) {
try {
+ flappingSchema.validate(newFlappingProperties);
verifyFlappingSettings(newFlappingProperties);
} catch (e) {
this.logger.error(
@@ -81,14 +101,16 @@ export class RulesSettingsFlappingClient {
}
const { attributes, version } = await this.getOrCreate();
- const modificationMetadata = await this.getModificationMetadata();
+ if (!attributes.flapping) {
+ throw new Error('Flapping settings are undefined');
+ }
+ const modificationMetadata = await this.getModificationMetadata();
try {
const result = await this.savedObjectsClient.update(
RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
{
- ...attributes,
flapping: {
...attributes.flapping,
...newFlappingProperties,
@@ -107,4 +129,55 @@ export class RulesSettingsFlappingClient {
throw Boom.boomify(e, { message: errorMessage });
}
}
+
+ public async getSettings(): Promise> {
+ try {
+ return await this.savedObjectsClient.get(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID
+ );
+ } catch (e) {
+ this.logger.error(`Failed to get flapping rules setting for current space. Error: ${e}`);
+ throw e;
+ }
+ }
+
+ public async createSettings(): Promise> {
+ const modificationMetadata = await this.getModificationMetadata();
+ try {
+ return await this.savedObjectsClient.create(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ flapping: {
+ ...DEFAULT_FLAPPING_SETTINGS,
+ ...modificationMetadata,
+ },
+ },
+ {
+ id: RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+ } catch (e) {
+ this.logger.error(`Failed to create flapping rules setting for current space. Error: ${e}`);
+ throw e;
+ }
+ }
+
+ /**
+ * Helper function to ensure that a rules-settings saved object always exists.
+ * Ensures the creation of the saved object is done lazily during retrieval.
+ */
+ private async getOrCreate(): Promise> {
+ try {
+ return await this.getSettings();
+ } catch (e) {
+ if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
+ this.logger.info('Creating new default flapping rules settings for current space.');
+ return await this.createSettings();
+ }
+ this.logger.error(`Failed to get flapping rules setting for current space. Error: ${e}`);
+ throw e;
+ }
+ }
}
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/index.ts b/x-pack/plugins/alerting/server/rules_settings_client/index.ts
index efbb3f0b3ccfe..fcbf30b0bcb6c 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client/index.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client/index.ts
@@ -7,3 +7,4 @@
export * from './rules_settings_client';
export * from './flapping/rules_settings_flapping_client';
+export * from './query_delay/rules_settings_query_delay_client';
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts
new file mode 100644
index 0000000000000..213ece8cd6fe4
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts
@@ -0,0 +1,385 @@
+/*
+ * 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 { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
+import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
+import {
+ RULES_SETTINGS_FEATURE_ID,
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ RulesSettings,
+ DEFAULT_QUERY_DELAY_SETTINGS,
+} from '../../../common';
+import {
+ RulesSettingsQueryDelayClient,
+ RulesSettingsQueryDelayClientConstructorOptions,
+} from './rules_settings_query_delay_client';
+
+const mockDateString = '2019-02-12T21:01:22.479Z';
+
+const savedObjectsClient = savedObjectsClientMock.create();
+
+const getMockRulesSettings = (): RulesSettings => {
+ return {
+ queryDelay: {
+ delay: DEFAULT_QUERY_DELAY_SETTINGS.delay,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: '2023-03-24T00:00:00.000Z',
+ updatedAt: '2023-03-24T00:00:00.000Z',
+ },
+ };
+};
+
+const rulesSettingsQueryDelayClientParams: jest.Mocked =
+ {
+ logger: loggingSystemMock.create().get(),
+ isServerless: false,
+ getModificationMetadata: jest.fn(),
+ savedObjectsClient,
+ };
+
+const updatedMetadata = {
+ createdAt: '2023-03-26T00:00:00.000Z',
+ updatedAt: '2023-03-26T00:00:00.000Z',
+ createdBy: 'updated-user',
+ updatedBy: 'updated-user',
+};
+
+describe('RulesSettingsQueryDelayClient', () => {
+ beforeEach(() => {
+ rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValue(updatedMetadata);
+ savedObjectsClient.get.mockResolvedValue({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: getMockRulesSettings(),
+ references: [],
+ version: '123',
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date(mockDateString));
+ });
+
+ afterAll(() => {
+ jest.clearAllMocks();
+ jest.useRealTimers();
+ });
+
+ test('can get query delay settings', async () => {
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+ const result = await client.get();
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ delay: DEFAULT_QUERY_DELAY_SETTINGS.delay,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ })
+ );
+ });
+
+ test('can update query delay settings', async () => {
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+
+ const mockResolve = {
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: getMockRulesSettings(),
+ references: [],
+ version: '123',
+ };
+
+ savedObjectsClient.update.mockResolvedValueOnce({
+ ...mockResolve,
+ attributes: {
+ queryDelay: {
+ ...mockResolve.attributes.queryDelay,
+ delay: 19,
+ },
+ },
+ });
+
+ const result = await client.update({
+ delay: 19,
+ });
+
+ expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ {
+ queryDelay: expect.objectContaining({
+ delay: 19,
+ updatedAt: '2023-03-26T00:00:00.000Z',
+ updatedBy: 'updated-user',
+ createdBy: 'test name',
+ createdAt: '2023-03-24T00:00:00.000Z',
+ }),
+ },
+ { version: '123' }
+ );
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ delay: 19,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ })
+ );
+ });
+
+ test('throws if savedObjectsClient failed to update', async () => {
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+ savedObjectsClient.update.mockRejectedValueOnce(new Error('failed!!'));
+
+ await expect(
+ client.update({
+ delay: 19,
+ })
+ ).rejects.toThrowError(
+ 'savedObjectsClient errored trying to update query delay settings: failed!!'
+ );
+ });
+
+ test('throws if new query delay setting fails verification', async () => {
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+ await expect(
+ client.update({
+ delay: 200,
+ })
+ ).rejects.toThrowError('Invalid query delay value, must be between 0 and 60, but got: 200.');
+ });
+
+ test('can create a new query delay settings saved object', async () => {
+ rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+
+ const result = await client.createSettings();
+
+ expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ queryDelay: expect.objectContaining({
+ delay: 0,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ {
+ id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+ expect(result.attributes).toEqual(mockAttributes);
+ });
+
+ test('can create a new query delay settings saved object with default serverless value', async () => {
+ rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsQueryDelayClient({
+ ...rulesSettingsQueryDelayClientParams,
+ isServerless: true,
+ });
+
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+
+ const result = await client.createSettings();
+
+ expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ queryDelay: expect.objectContaining({
+ delay: 15,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ {
+ id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+ expect(result.attributes).toEqual(mockAttributes);
+ });
+
+ test('can get existing query delay settings saved object', async () => {
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+ const result = await client.getSettings();
+ expect(result.attributes).toEqual(mockAttributes);
+ });
+
+ test('throws if there is no existing saved object to get', async () => {
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+
+ savedObjectsClient.get.mockRejectedValueOnce(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
+ )
+ );
+ await expect(client.get()).rejects.toThrowError();
+ });
+
+ test('can persist query delay settings when saved object already exists', async () => {
+ rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ });
+
+ const result = await client.get();
+
+ expect(savedObjectsClient.get).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
+ );
+ expect(savedObjectsClient.create).not.toHaveBeenCalled();
+ expect(result).toEqual(mockAttributes.queryDelay);
+ });
+
+ test('can update query delay settings when saved object does not exist', async () => {
+ rulesSettingsQueryDelayClientParams.getModificationMetadata.mockResolvedValueOnce({
+ ...updatedMetadata,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ });
+ const client = new RulesSettingsQueryDelayClient(rulesSettingsQueryDelayClientParams);
+ const mockAttributes = getMockRulesSettings();
+
+ savedObjectsClient.get.mockRejectedValueOnce(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
+ )
+ );
+
+ const mockResolve = {
+ id: RULES_SETTINGS_FEATURE_ID,
+ type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ attributes: mockAttributes,
+ references: [],
+ version: '123',
+ };
+
+ savedObjectsClient.create.mockResolvedValueOnce(mockResolve);
+ savedObjectsClient.update.mockResolvedValueOnce({
+ ...mockResolve,
+ attributes: {
+ queryDelay: {
+ ...mockResolve.attributes.queryDelay,
+ delay: 5,
+ },
+ },
+ });
+
+ // Try to update with new values
+ const result = await client.update({
+ delay: 5,
+ });
+
+ // Tried to get first, but no results
+ expect(savedObjectsClient.get).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
+ );
+
+ // So create a new entry
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ queryDelay: expect.objectContaining({
+ delay: mockAttributes.queryDelay?.delay,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ {
+ id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+
+ // Try to update with version
+ expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ {
+ queryDelay: expect.objectContaining({
+ delay: 5,
+ createdBy: 'test name',
+ updatedBy: 'test name',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ }),
+ },
+ { version: '123' }
+ );
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ delay: 5,
+ })
+ );
+ });
+});
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts
new file mode 100644
index 0000000000000..ac394dca17180
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts
@@ -0,0 +1,179 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import {
+ Logger,
+ SavedObjectsClientContract,
+ SavedObject,
+ SavedObjectsErrorHelpers,
+} from '@kbn/core/server';
+import {
+ RulesSettings,
+ RulesSettingsModificationMetadata,
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ RulesSettingsQueryDelayProperties,
+ MIN_QUERY_DELAY,
+ MAX_QUERY_DELAY,
+ RulesSettingsQueryDelay,
+ DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS,
+ DEFAULT_QUERY_DELAY_SETTINGS,
+} from '../../../common';
+import { retryIfConflicts } from '../../lib/retry_if_conflicts';
+import { queryDelaySchema } from '../schemas';
+
+const verifyQueryDelaySettings = (settings: RulesSettingsQueryDelayProperties) => {
+ const { delay } = settings;
+
+ if (delay < MIN_QUERY_DELAY || delay > MAX_QUERY_DELAY) {
+ throw Boom.badRequest(
+ `Invalid query delay value, must be between ${MIN_QUERY_DELAY} and ${MAX_QUERY_DELAY}, but got: ${delay}.`
+ );
+ }
+};
+
+export interface RulesSettingsQueryDelayClientConstructorOptions {
+ readonly logger: Logger;
+ readonly savedObjectsClient: SavedObjectsClientContract;
+ readonly isServerless: boolean;
+ readonly getModificationMetadata: () => Promise;
+}
+
+export class RulesSettingsQueryDelayClient {
+ private readonly logger: Logger;
+ private readonly savedObjectsClient: SavedObjectsClientContract;
+ private readonly isServerless: boolean;
+ private readonly getModificationMetadata: () => Promise;
+
+ constructor(options: RulesSettingsQueryDelayClientConstructorOptions) {
+ this.logger = options.logger;
+ this.savedObjectsClient = options.savedObjectsClient;
+ this.isServerless = options.isServerless;
+ this.getModificationMetadata = options.getModificationMetadata;
+ }
+
+ public async get(): Promise {
+ const rulesSettings = await this.getOrCreate();
+ if (!rulesSettings.attributes.queryDelay) {
+ this.logger.error('Failed to get query delay rules setting for current space.');
+ throw new Error(
+ 'Failed to get query delay rules setting for current space. Query delay settings are undefined'
+ );
+ }
+ return rulesSettings.attributes.queryDelay;
+ }
+
+ public async update(newQueryDelayProperties: RulesSettingsQueryDelayProperties) {
+ return await retryIfConflicts(
+ this.logger,
+ 'ruleSettingsClient.queryDelay.update()',
+ async () => await this.updateWithOCC(newQueryDelayProperties)
+ );
+ }
+
+ private async updateWithOCC(newQueryDelayProperties: RulesSettingsQueryDelayProperties) {
+ try {
+ queryDelaySchema.validate(newQueryDelayProperties);
+ verifyQueryDelaySettings(newQueryDelayProperties);
+ } catch (e) {
+ this.logger.error(
+ `Failed to verify new query delay settings properties when updating. Error: ${e}`
+ );
+ throw e;
+ }
+
+ const { attributes, version } = await this.getOrCreate();
+ if (!attributes.queryDelay) {
+ throw new Error('Query delay settings are undefined');
+ }
+
+ const modificationMetadata = await this.getModificationMetadata();
+ try {
+ const result = await this.savedObjectsClient.update(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ {
+ queryDelay: {
+ ...attributes.queryDelay,
+ ...newQueryDelayProperties,
+ updatedAt: modificationMetadata.updatedAt,
+ updatedBy: modificationMetadata.updatedBy,
+ },
+ },
+ {
+ version,
+ }
+ );
+
+ if (!result.attributes.queryDelay) {
+ throw new Error('Query delay settings are undefined');
+ }
+ return result.attributes.queryDelay;
+ } catch (e) {
+ const errorMessage = 'savedObjectsClient errored trying to update query delay settings';
+ this.logger.error(`${errorMessage}: ${e}`);
+ throw Boom.boomify(e, { message: errorMessage });
+ }
+ }
+
+ public async getSettings(): Promise> {
+ try {
+ return await this.savedObjectsClient.get(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID
+ );
+ } catch (e) {
+ this.logger.error(`Failed to get query delay rules setting for current space. Error: ${e}`);
+ throw e;
+ }
+ }
+
+ public async createSettings(): Promise> {
+ const modificationMetadata = await this.getModificationMetadata();
+ const defaultQueryDelaySettings = this.isServerless
+ ? DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS
+ : DEFAULT_QUERY_DELAY_SETTINGS;
+ try {
+ return await this.savedObjectsClient.create(
+ RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ {
+ queryDelay: {
+ ...defaultQueryDelaySettings,
+ ...modificationMetadata,
+ },
+ },
+ {
+ id: RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID,
+ overwrite: true,
+ }
+ );
+ } catch (e) {
+ this.logger.error(
+ `Failed to create query delay rules setting for current space. Error: ${e}`
+ );
+ throw e;
+ }
+ }
+
+ /**
+ * Helper function to ensure that a rules-settings saved object always exists.
+ * Ensures the creation of the saved object is done lazily during retrieval.
+ */
+ private async getOrCreate(): Promise> {
+ try {
+ return await this.getSettings();
+ } catch (e) {
+ if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
+ this.logger.info('Creating new default query delay rules settings for current space.');
+ return await this.createSettings();
+ }
+ this.logger.error(`Failed to get query delay rules setting for current space. Error: ${e}`);
+ throw e;
+ }
+ }
+}
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.test.ts b/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.test.ts
index a40c491b9117e..314e28cd6f245 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.test.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.test.ts
@@ -11,16 +11,7 @@ import {
} from './rules_settings_client';
import { RulesSettingsFlappingClient } from './flapping/rules_settings_flapping_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
-import { SavedObjectsErrorHelpers } from '@kbn/core/server';
-import {
- RULES_SETTINGS_FEATURE_ID,
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID,
- DEFAULT_FLAPPING_SETTINGS,
- RulesSettings,
-} from '../../common';
-
-const mockDateString = '2019-02-12T21:01:22.479Z';
+import { RulesSettingsQueryDelayClient } from './query_delay/rules_settings_query_delay_client';
const savedObjectsClient = savedObjectsClientMock.create();
@@ -28,258 +19,17 @@ const rulesSettingsClientParams: jest.Mocked {
- return {
- flapping: {
- enabled: DEFAULT_FLAPPING_SETTINGS.enabled,
- lookBackWindow: DEFAULT_FLAPPING_SETTINGS.lookBackWindow,
- statusChangeThreshold: DEFAULT_FLAPPING_SETTINGS.statusChangeThreshold,
- createdBy: 'test name',
- updatedBy: 'test name',
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- },
- };
+ isServerless: false,
};
describe('RulesSettingsClient', () => {
- beforeAll(() => {
- jest.useFakeTimers();
- jest.setSystemTime(new Date(mockDateString));
- });
-
afterAll(() => {
- jest.useRealTimers();
- });
-
- beforeEach(() => {
jest.resetAllMocks();
- rulesSettingsClientParams.getUserName.mockResolvedValue('test name');
});
test('can initialize correctly', async () => {
const client = new RulesSettingsClient(rulesSettingsClientParams);
expect(client.flapping()).toEqual(expect.any(RulesSettingsFlappingClient));
- });
-
- test('can create a new rules settings saved object', async () => {
- const client = new RulesSettingsClient(rulesSettingsClientParams);
- const mockAttributes = getMockRulesSettings();
-
- savedObjectsClient.create.mockResolvedValueOnce({
- id: RULES_SETTINGS_FEATURE_ID,
- type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
- attributes: mockAttributes,
- references: [],
- });
-
- const result = await client.create();
-
- expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.create).toHaveBeenCalledWith(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- {
- flapping: expect.objectContaining({
- enabled: mockAttributes.flapping.enabled,
- lookBackWindow: mockAttributes.flapping.lookBackWindow,
- statusChangeThreshold: mockAttributes.flapping.statusChangeThreshold,
- createdBy: 'test name',
- updatedBy: 'test name',
- createdAt: expect.any(String),
- updatedAt: expect.any(String),
- }),
- },
- {
- id: RULES_SETTINGS_SAVED_OBJECT_ID,
- overwrite: true,
- }
- );
- expect(result.attributes).toEqual(mockAttributes);
- });
-
- test('can get existing rules settings saved object', async () => {
- const client = new RulesSettingsClient(rulesSettingsClientParams);
- const mockAttributes = getMockRulesSettings();
-
- savedObjectsClient.get.mockResolvedValueOnce({
- id: RULES_SETTINGS_FEATURE_ID,
- type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
- attributes: mockAttributes,
- references: [],
- });
- const result = await client.get();
- expect(result.attributes).toEqual(mockAttributes);
- });
-
- test('throws if there is no existing saved object to get', async () => {
- const client = new RulesSettingsClient(rulesSettingsClientParams);
-
- savedObjectsClient.get.mockRejectedValueOnce(
- SavedObjectsErrorHelpers.createGenericNotFoundError(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID
- )
- );
- await expect(client.get()).rejects.toThrowError();
- });
-
- test('can persist flapping settings when saved object does not exist', async () => {
- const client = new RulesSettingsClient(rulesSettingsClientParams);
- const mockAttributes = getMockRulesSettings();
- savedObjectsClient.get.mockRejectedValueOnce(
- SavedObjectsErrorHelpers.createGenericNotFoundError(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID
- )
- );
-
- savedObjectsClient.create.mockResolvedValueOnce({
- id: RULES_SETTINGS_FEATURE_ID,
- type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
- attributes: mockAttributes,
- references: [],
- });
-
- const result = await client.flapping().get();
-
- expect(savedObjectsClient.get).toHaveBeenCalledWith(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID
- );
-
- expect(savedObjectsClient.create).toHaveBeenCalledWith(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- {
- flapping: expect.objectContaining({
- enabled: mockAttributes.flapping.enabled,
- lookBackWindow: mockAttributes.flapping.lookBackWindow,
- statusChangeThreshold: mockAttributes.flapping.statusChangeThreshold,
- createdBy: 'test name',
- updatedBy: 'test name',
- createdAt: expect.any(String),
- updatedAt: expect.any(String),
- }),
- },
- {
- id: RULES_SETTINGS_SAVED_OBJECT_ID,
- overwrite: true,
- }
- );
- expect(result).toEqual(mockAttributes.flapping);
- });
-
- test('can persist flapping settings when saved object already exists', async () => {
- const client = new RulesSettingsClient(rulesSettingsClientParams);
- const mockAttributes = getMockRulesSettings();
-
- savedObjectsClient.get.mockResolvedValueOnce({
- id: RULES_SETTINGS_FEATURE_ID,
- type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
- attributes: mockAttributes,
- references: [],
- });
-
- const result = await client.flapping().get();
-
- expect(savedObjectsClient.get).toHaveBeenCalledWith(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID
- );
- expect(savedObjectsClient.create).not.toHaveBeenCalled();
- expect(result).toEqual(mockAttributes.flapping);
- });
-
- test('can update flapping settings when saved object does not exist', async () => {
- const client = new RulesSettingsClient(rulesSettingsClientParams);
- const mockAttributes = getMockRulesSettings();
-
- savedObjectsClient.get.mockRejectedValueOnce(
- SavedObjectsErrorHelpers.createGenericNotFoundError(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID
- )
- );
-
- const mockResolve = {
- id: RULES_SETTINGS_FEATURE_ID,
- type: RULES_SETTINGS_SAVED_OBJECT_TYPE,
- attributes: mockAttributes,
- references: [],
- version: '123',
- };
-
- savedObjectsClient.create.mockResolvedValueOnce(mockResolve);
- savedObjectsClient.update.mockResolvedValueOnce({
- ...mockResolve,
- attributes: {
- flapping: {
- ...mockResolve.attributes.flapping,
- enabled: false,
- lookBackWindow: 5,
- statusChangeThreshold: 5,
- },
- },
- });
-
- // Try to update with new values
- const result = await client.flapping().update({
- enabled: false,
- lookBackWindow: 5,
- statusChangeThreshold: 5,
- });
-
- // Tried to get first, but no results
- expect(savedObjectsClient.get).toHaveBeenCalledWith(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID
- );
-
- // So create a new entry
- expect(savedObjectsClient.create).toHaveBeenCalledWith(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- {
- flapping: expect.objectContaining({
- enabled: mockAttributes.flapping.enabled,
- lookBackWindow: mockAttributes.flapping.lookBackWindow,
- statusChangeThreshold: mockAttributes.flapping.statusChangeThreshold,
- createdBy: 'test name',
- updatedBy: 'test name',
- createdAt: expect.any(String),
- updatedAt: expect.any(String),
- }),
- },
- {
- id: RULES_SETTINGS_SAVED_OBJECT_ID,
- overwrite: true,
- }
- );
-
- // Try to update with version
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID,
- {
- flapping: expect.objectContaining({
- enabled: false,
- lookBackWindow: 5,
- statusChangeThreshold: 5,
- createdBy: 'test name',
- updatedBy: 'test name',
- createdAt: expect.any(String),
- updatedAt: expect.any(String),
- }),
- },
- { version: '123' }
- );
-
- expect(result).toEqual(
- expect.objectContaining({
- enabled: false,
- lookBackWindow: 5,
- statusChangeThreshold: 5,
- })
- );
+ expect(client.queryDelay()).toEqual(expect.any(RulesSettingsQueryDelayClient));
});
});
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.ts b/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.ts
index f723119d2de80..50e7650f42ff5 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client/rules_settings_client.ts
@@ -5,24 +5,15 @@
* 2.0.
*/
-import {
- Logger,
- SavedObjectsClientContract,
- SavedObject,
- SavedObjectsErrorHelpers,
-} from '@kbn/core/server';
+import { Logger, SavedObjectsClientContract } from '@kbn/core/server';
import { RulesSettingsFlappingClient } from './flapping/rules_settings_flapping_client';
-import {
- RulesSettings,
- DEFAULT_FLAPPING_SETTINGS,
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID,
-} from '../../common';
+import { RulesSettingsQueryDelayClient } from './query_delay/rules_settings_query_delay_client';
export interface RulesSettingsClientConstructorOptions {
readonly logger: Logger;
readonly savedObjectsClient: SavedObjectsClientContract;
readonly getUserName: () => Promise;
+ readonly isServerless: boolean;
}
export class RulesSettingsClient {
@@ -30,16 +21,25 @@ export class RulesSettingsClient {
private readonly savedObjectsClient: SavedObjectsClientContract;
private readonly getUserName: () => Promise;
private readonly _flapping: RulesSettingsFlappingClient;
+ private readonly _queryDelay: RulesSettingsQueryDelayClient;
+ private readonly isServerless: boolean;
constructor(options: RulesSettingsClientConstructorOptions) {
this.logger = options.logger;
this.savedObjectsClient = options.savedObjectsClient;
this.getUserName = options.getUserName;
+ this.isServerless = options.isServerless;
this._flapping = new RulesSettingsFlappingClient({
logger: this.logger,
savedObjectsClient: this.savedObjectsClient,
- getOrCreate: this.getOrCreate.bind(this),
+ getModificationMetadata: this.getModificationMetadata.bind(this),
+ });
+
+ this._queryDelay = new RulesSettingsQueryDelayClient({
+ logger: this.logger,
+ savedObjectsClient: this.savedObjectsClient,
+ isServerless: this.isServerless,
getModificationMetadata: this.getModificationMetadata.bind(this),
});
}
@@ -56,59 +56,11 @@ export class RulesSettingsClient {
};
}
- public async get(): Promise> {
- try {
- return await this.savedObjectsClient.get(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- RULES_SETTINGS_SAVED_OBJECT_ID
- );
- } catch (e) {
- this.logger.error(`Failed to get rules setting for current space. Error: ${e}`);
- throw e;
- }
- }
-
- public async create(): Promise> {
- const modificationMetadata = await this.getModificationMetadata();
-
- try {
- return await this.savedObjectsClient.create(
- RULES_SETTINGS_SAVED_OBJECT_TYPE,
- {
- flapping: {
- ...DEFAULT_FLAPPING_SETTINGS,
- ...modificationMetadata,
- },
- },
- {
- id: RULES_SETTINGS_SAVED_OBJECT_ID,
- overwrite: true,
- }
- );
- } catch (e) {
- this.logger.error(`Failed to create rules setting for current space. Error: ${e}`);
- throw e;
- }
- }
-
- /**
- * Helper function to ensure that a rules-settings saved object always exists.
- * Ensures the creation of the saved object is done lazily during retrieval.
- */
- private async getOrCreate(): Promise> {
- try {
- return await this.get();
- } catch (e) {
- if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
- this.logger.info('Creating new default rules settings for current space.');
- return await this.create();
- }
- this.logger.error(`Failed to persist rules setting for current space. Error: ${e}`);
- throw e;
- }
- }
-
public flapping(): RulesSettingsFlappingClient {
return this._flapping;
}
+
+ public queryDelay(): RulesSettingsQueryDelayClient {
+ return this._queryDelay;
+ }
}
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/schemas/flapping_schema.ts b/x-pack/plugins/alerting/server/rules_settings_client/schemas/flapping_schema.ts
new file mode 100644
index 0000000000000..a9765fe826bef
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_settings_client/schemas/flapping_schema.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+
+export const flappingSchema = schema.object({
+ enabled: schema.boolean(),
+ lookBackWindow: schema.number(),
+ statusChangeThreshold: schema.number(),
+});
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/schemas/index.ts b/x-pack/plugins/alerting/server/rules_settings_client/schemas/index.ts
new file mode 100644
index 0000000000000..03ee6f939a233
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_settings_client/schemas/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+
+export { flappingSchema } from './flapping_schema';
+export { queryDelaySchema } from './query_delay_schema';
diff --git a/x-pack/plugins/alerting/server/rules_settings_client/schemas/query_delay_schema.ts b/x-pack/plugins/alerting/server/rules_settings_client/schemas/query_delay_schema.ts
new file mode 100644
index 0000000000000..613dd9646846f
--- /dev/null
+++ b/x-pack/plugins/alerting/server/rules_settings_client/schemas/query_delay_schema.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+
+export const queryDelaySchema = schema.object({
+ delay: schema.number(),
+});
diff --git a/x-pack/plugins/alerting/server/rules_settings_client_factory.test.ts b/x-pack/plugins/alerting/server/rules_settings_client_factory.test.ts
index a91e6697a4d8c..bb278dbf50cdd 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client_factory.test.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client_factory.test.ts
@@ -30,6 +30,7 @@ const securityPluginStart = securityMock.createStart();
const rulesSettingsClientFactoryParams: jest.Mocked = {
logger: loggingSystemMock.create().get(),
savedObjectsService,
+ isServerless: false,
};
beforeEach(() => {
@@ -58,6 +59,7 @@ test('creates a rules settings client with proper constructor arguments when sec
logger: rulesSettingsClientFactoryParams.logger,
savedObjectsClient,
getUserName: expect.any(Function),
+ isServerless: false,
});
});
@@ -80,6 +82,7 @@ test('creates a rules settings client with proper constructor arguments', async
logger: rulesSettingsClientFactoryParams.logger,
savedObjectsClient,
getUserName: expect.any(Function),
+ isServerless: false,
});
});
@@ -106,6 +109,7 @@ test('creates an unauthorized rules settings client', async () => {
logger: rulesSettingsClientFactoryParams.logger,
savedObjectsClient,
getUserName: expect.any(Function),
+ isServerless: false,
});
});
diff --git a/x-pack/plugins/alerting/server/rules_settings_client_factory.ts b/x-pack/plugins/alerting/server/rules_settings_client_factory.ts
index 619e498c6b988..f69068ee3cb65 100644
--- a/x-pack/plugins/alerting/server/rules_settings_client_factory.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_client_factory.ts
@@ -18,6 +18,7 @@ import { RULES_SETTINGS_SAVED_OBJECT_TYPE } from '../common';
export interface RulesSettingsClientFactoryOpts {
logger: Logger;
savedObjectsService: SavedObjectsServiceStart;
+ isServerless: boolean;
securityPluginStart?: SecurityPluginStart;
}
@@ -26,6 +27,7 @@ export class RulesSettingsClientFactory {
private logger!: Logger;
private savedObjectsService!: SavedObjectsServiceStart;
private securityPluginStart?: SecurityPluginStart;
+ private isServerless = false;
public initialize(options: RulesSettingsClientFactoryOpts) {
if (this.isInitialized) {
@@ -35,6 +37,7 @@ export class RulesSettingsClientFactory {
this.logger = options.logger;
this.savedObjectsService = options.savedObjectsService;
this.securityPluginStart = options.securityPluginStart;
+ this.isServerless = options.isServerless;
}
private createRulesSettingsClient(request: KibanaRequest, withAuth: boolean) {
@@ -54,6 +57,7 @@ export class RulesSettingsClientFactory {
const user = securityPluginStart.authc.getCurrentUser(request);
return user ? user.username : null;
},
+ isServerless: this.isServerless,
});
}
diff --git a/x-pack/plugins/alerting/server/rules_settings_feature.ts b/x-pack/plugins/alerting/server/rules_settings_feature.ts
index 5c420fd32bd3c..2d39b9290a0cd 100644
--- a/x-pack/plugins/alerting/server/rules_settings_feature.ts
+++ b/x-pack/plugins/alerting/server/rules_settings_feature.ts
@@ -14,6 +14,8 @@ import {
ALL_FLAPPING_SETTINGS_SUB_FEATURE_ID,
API_PRIVILEGES,
RULES_SETTINGS_SAVED_OBJECT_TYPE,
+ ALL_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
+ READ_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
} from '../common';
export const rulesSettingsFeature: KibanaFeatureConfig = {
@@ -87,5 +89,42 @@ export const rulesSettingsFeature: KibanaFeatureConfig = {
},
],
},
+ {
+ name: i18n.translate('xpack.alerting.feature.queryDelaySettingsSubFeatureName', {
+ defaultMessage: 'Query delay',
+ }),
+ privilegeGroups: [
+ {
+ groupType: 'mutually_exclusive',
+ privileges: [
+ {
+ api: [
+ API_PRIVILEGES.READ_QUERY_DELAY_SETTINGS,
+ API_PRIVILEGES.WRITE_QUERY_DELAY_SETTINGS,
+ ],
+ name: 'All',
+ id: ALL_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
+ includeIn: 'all',
+ savedObject: {
+ all: [RULES_SETTINGS_SAVED_OBJECT_TYPE],
+ read: [],
+ },
+ ui: ['writeQueryDelaySettingsUI', 'readQueryDelaySettingsUI'],
+ },
+ {
+ api: [API_PRIVILEGES.READ_QUERY_DELAY_SETTINGS],
+ name: 'Read',
+ id: READ_QUERY_DELAY_SETTINGS_SUB_FEATURE_ID,
+ includeIn: 'read',
+ savedObject: {
+ all: [],
+ read: [RULES_SETTINGS_SAVED_OBJECT_TYPE],
+ },
+ ui: ['readQueryDelaySettingsUI'],
+ },
+ ],
+ },
+ ],
+ },
],
};
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts
index 4dd391cc4f801..3cc9c2359c272 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts
@@ -80,6 +80,7 @@ import { RuleResultService } from '../monitoring/rule_result_service';
import { LegacyAlertsClient } from '../alerts_client';
import { IAlertsClient } from '../alerts_client/types';
import { MaintenanceWindow } from '../application/maintenance_window/types';
+import { getTimeRange } from '../lib/get_time_range';
const FALLBACK_RETRY_INTERVAL = '5m';
const CONNECTIVITY_RETRY_INTERVAL = '5m';
@@ -324,6 +325,7 @@ export class TaskRunner<
const rulesSettingsClient = this.context.getRulesSettingsClientWithRequest(fakeRequest);
const flappingSettings = await rulesSettingsClient.flapping().get();
+ const queryDelaySettings = await rulesSettingsClient.queryDelay().get();
const alertsClientParams = {
logger: this.logger,
@@ -514,6 +516,8 @@ export class TaskRunner<
logger: this.logger,
flappingSettings,
...(maintenanceWindowIds.length ? { maintenanceWindowIds } : {}),
+ getTimeRange: (timeWindow) =>
+ getTimeRange(this.logger, queryDelaySettings, timeWindow),
})
);
diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts
index 66e2c3bfa6069..26eed5f254bc9 100644
--- a/x-pack/plugins/alerting/server/types.ts
+++ b/x-pack/plugins/alerting/server/types.ts
@@ -28,7 +28,11 @@ import { Filter } from '@kbn/es-query';
import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { RulesClient } from './rules_client';
-import { RulesSettingsClient, RulesSettingsFlappingClient } from './rules_settings_client';
+import {
+ RulesSettingsClient,
+ RulesSettingsFlappingClient,
+ RulesSettingsQueryDelayClient,
+} from './rules_settings_client';
import { MaintenanceWindowClient } from './maintenance_window_client';
export * from '../common';
import {
@@ -135,6 +139,7 @@ export interface RuleExecutorOptions<
namespace?: string;
flappingSettings: RulesSettingsFlappingProperties;
maintenanceWindowIds?: string[];
+ getTimeRange: (timeWindow?: string) => { dateStart: string; dateEnd: string };
}
export interface RuleParamsAndRefs {
@@ -372,6 +377,7 @@ export type RulesClientApi = PublicMethodsOf;
export type RulesSettingsClientApi = PublicMethodsOf;
export type RulesSettingsFlappingClientApi = PublicMethodsOf;
+export type RulesSettingsQueryDelayClientApi = PublicMethodsOf;
export type MaintenanceWindowClientApi = PublicMethodsOf;
diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts
index 611ca43499c6a..28f08cdc72811 100644
--- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.test.ts
@@ -103,7 +103,11 @@ describe('Transaction duration anomaly alert', () => {
ml,
});
- const params = { anomalySeverityType: ML_ANOMALY_SEVERITY.MINOR };
+ const params = {
+ anomalySeverityType: ML_ANOMALY_SEVERITY.MINOR,
+ windowSize: 5,
+ windowUnit: 'm',
+ };
await executor({ params });
diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts
index d318f6ec4a44d..b74db63061306 100644
--- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts
@@ -97,7 +97,13 @@ export function registerAnomalyRuleType({
producer: 'apm',
minimumLicenseRequired: 'basic',
isExportable: true,
- executor: async ({ params, services, spaceId, startedAt }) => {
+ executor: async ({
+ params,
+ services,
+ spaceId,
+ startedAt,
+ getTimeRange,
+ }) => {
if (!ml) {
return { state: {} };
}
@@ -144,12 +150,14 @@ export function registerAnomalyRuleType({
}
// start time must be at least 30, does like this to support rules created before this change where default was 15
- const startTime = Math.min(
- datemath.parse('now-30m')!.valueOf(),
+ const window =
+ datemath.parse('now-30m')!.valueOf() >
datemath
- .parse(`now-${ruleParams.windowSize}${ruleParams.windowUnit}`)
- ?.valueOf() || 0
- );
+ .parse(`now-${ruleParams.windowSize}${ruleParams.windowUnit}`)!
+ .valueOf()
+ ? '30m'
+ : `${ruleParams.windowSize}${ruleParams.windowUnit}`;
+ const { dateStart } = getTimeRange(window);
const jobIds = mlJobs.map((job) => job.jobId);
const anomalySearchParams = {
@@ -165,7 +173,7 @@ export function registerAnomalyRuleType({
{
range: {
timestamp: {
- gte: startTime,
+ gte: dateStart,
format: 'epoch_millis',
},
},
diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts
index 4ecade37780d9..6a0a5b8fdbbe6 100644
--- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts
@@ -104,6 +104,7 @@ export function registerErrorCountRuleType({
services,
spaceId,
startedAt,
+ getTimeRange,
}) => {
const allGroupByFields = getAllGroupByFields(
ApmRuleType.ErrorCount,
@@ -131,6 +132,10 @@ export function registerErrorCountRuleType({
]
: [];
+ const { dateStart } = getTimeRange(
+ `${ruleParams.windowSize}${ruleParams.windowUnit}`
+ );
+
const searchParams = {
index: indices.error,
body: {
@@ -142,7 +147,7 @@ export function registerErrorCountRuleType({
{
range: {
'@timestamp': {
- gte: `now-${ruleParams.windowSize}${ruleParams.windowUnit}`,
+ gte: dateStart,
},
},
},
diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts
index cbfb5db627100..3789a55e6e4e0 100644
--- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts
@@ -111,7 +111,12 @@ export function registerTransactionDurationRuleType({
producer: APM_SERVER_FEATURE_ID,
minimumLicenseRequired: 'basic',
isExportable: true,
- executor: async ({ params: ruleParams, services, spaceId }) => {
+ executor: async ({
+ params: ruleParams,
+ services,
+ spaceId,
+ getTimeRange,
+ }) => {
const allGroupByFields = getAllGroupByFields(
ApmRuleType.TransactionDuration,
ruleParams.groupBy
@@ -152,6 +157,10 @@ export function registerTransactionDurationRuleType({
]
: [];
+ const { dateStart } = getTimeRange(
+ `${ruleParams.windowSize}${ruleParams.windowUnit}`
+ );
+
const searchParams = {
index,
body: {
@@ -163,7 +172,7 @@ export function registerTransactionDurationRuleType({
{
range: {
'@timestamp': {
- gte: `now-${ruleParams.windowSize}${ruleParams.windowUnit}`,
+ gte: dateStart,
},
},
},
diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts
index d7c700c42071c..7e3c7bce8baf3 100644
--- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts
@@ -113,6 +113,7 @@ export function registerTransactionErrorRateRuleType({
spaceId,
params: ruleParams,
startedAt,
+ getTimeRange,
}) => {
const allGroupByFields = getAllGroupByFields(
ApmRuleType.TransactionErrorRate,
@@ -154,6 +155,10 @@ export function registerTransactionErrorRateRuleType({
]
: [];
+ const { dateStart } = getTimeRange(
+ `${ruleParams.windowSize}${ruleParams.windowUnit}`
+ );
+
const searchParams = {
index,
body: {
@@ -165,7 +170,7 @@ export function registerTransactionErrorRateRuleType({
{
range: {
'@timestamp': {
- gte: `now-${ruleParams.windowSize}${ruleParams.windowUnit}`,
+ gte: dateStart,
},
},
},
diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts
index 185af5a5496e7..b4b5692708456 100644
--- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts
@@ -95,6 +95,10 @@ export const createRuleTypeMocks = () => {
},
startedAt: new Date(),
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = new Date(Date.now()).toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
});
},
};
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts
index a47008a0fbaf1..29c4bfe0a159a 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts
@@ -83,6 +83,10 @@ const mockOptions = {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = new Date().toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
};
const setEvaluationResults = (response: Record) => {
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index b5b0c1973307b..f3bd6972eeea6 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -73,6 +73,10 @@ const mockOptions = {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = STARTED_AT_MOCK_DATE.toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
};
const setEvaluationResults = (response: Array>) => {
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
index 2803fcefcb6ee..46a39f397957e 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
@@ -127,6 +127,10 @@ const mockOptions = {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = STARTED_AT_MOCK_DATE.toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
};
const setEvaluationResults = (response: Array>) => {
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
index 1f57c215e51a7..972fcaec08892 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
@@ -127,6 +127,7 @@ export const createMetricThresholdExecutor = ({
executionId,
spaceId,
rule: { id: ruleId },
+ getTimeRange,
} = options;
const { criteria } = params;
@@ -191,6 +192,8 @@ export const createMetricThresholdExecutor = ({
throw new Error('The selected data view does not have a timestamp field');
}
+ // Calculate initial start and end date with no time window, as each criteria has it's own time window
+ const { dateStart, dateEnd } = getTimeRange();
const alertResults = await evaluateRule(
services.scopedClusterClient.asCurrentUser,
params as EvaluatedRuleParams,
@@ -199,8 +202,8 @@ export const createMetricThresholdExecutor = ({
compositeSize,
alertOnGroupDisappear,
logger,
+ { end: dateEnd, start: dateStart },
state.lastRunTimestamp,
- { end: startedAt.valueOf() },
convertStringsToMissingGroupsRecord(previousMissingGroups)
);
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.test.ts
index c48ce1d9ab50d..41109fbeae912 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.test.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.test.ts
@@ -11,123 +11,73 @@ import moment from 'moment';
import { createTimerange } from './create_timerange';
describe('createTimerange(interval, aggType, timeframe)', () => {
- describe('without timeframe', () => {
- describe('Basic Metric Aggs', () => {
- it('should return a second range for last 1 second', () => {
- const subject = createTimerange(1000, Aggregators.COUNT);
- expect(subject.end - subject.start).toEqual(1000);
- });
- it('should return a minute range for last 1 minute', () => {
- const subject = createTimerange(60000, Aggregators.COUNT);
- expect(subject.end - subject.start).toEqual(60000);
- });
- it('should return 5 minute range for last 5 minutes', () => {
- const subject = createTimerange(300000, Aggregators.COUNT);
- expect(subject.end - subject.start).toEqual(300000);
- });
- it('should return a hour range for last 1 hour', () => {
- const subject = createTimerange(3600000, Aggregators.COUNT);
- expect(subject.end - subject.start).toEqual(3600000);
- });
- it('should return a day range for last 1 day', () => {
- const subject = createTimerange(86400000, Aggregators.COUNT);
- expect(subject.end - subject.start).toEqual(86400000);
- });
+ const end = moment();
+ const timeframe = {
+ start: end.clone().toISOString(),
+ end: end.toISOString(),
+ };
+ describe('Basic Metric Aggs', () => {
+ it('should return a second range for last 1 second', () => {
+ const subject = createTimerange(1000, Aggregators.COUNT, timeframe);
+ expect(subject.end - subject.start).toEqual(1000);
});
- describe('Rate Aggs', () => {
- it('should return a 20 second range for last 1 second', () => {
- const subject = createTimerange(1000, Aggregators.RATE);
- expect(subject.end - subject.start).toEqual(1000 * 2);
- });
- it('should return a 5 minute range for last 1 minute', () => {
- const subject = createTimerange(60000, Aggregators.RATE);
- expect(subject.end - subject.start).toEqual(60000 * 2);
- });
- it('should return 25 minute range for last 5 minutes', () => {
- const subject = createTimerange(300000, Aggregators.RATE);
- expect(subject.end - subject.start).toEqual(300000 * 2);
- });
- it('should return 5 hour range for last hour', () => {
- const subject = createTimerange(3600000, Aggregators.RATE);
- expect(subject.end - subject.start).toEqual(3600000 * 2);
- });
- it('should return a 5 day range for last day', () => {
- const subject = createTimerange(86400000, Aggregators.RATE);
- expect(subject.end - subject.start).toEqual(86400000 * 2);
- });
+ it('should return a minute range for last 1 minute', () => {
+ const subject = createTimerange(60000, Aggregators.COUNT, timeframe);
+ expect(subject.end - subject.start).toEqual(60000);
+ });
+ it('should return 5 minute range for last 5 minutes', () => {
+ const subject = createTimerange(300000, Aggregators.COUNT, timeframe);
+ expect(subject.end - subject.start).toEqual(300000);
+ });
+ it('should return a hour range for last 1 hour', () => {
+ const subject = createTimerange(3600000, Aggregators.COUNT, timeframe);
+ expect(subject.end - subject.start).toEqual(3600000);
+ });
+ it('should return a day range for last 1 day', () => {
+ const subject = createTimerange(86400000, Aggregators.COUNT, timeframe);
+ expect(subject.end - subject.start).toEqual(86400000);
});
});
- describe('with full timeframe', () => {
- describe('Basic Metric Aggs', () => {
- it('should return 5 minute range when given 4 minute timeframe', () => {
- const end = moment();
- const timeframe = {
- start: end.clone().subtract(4, 'minutes').valueOf(),
- end: end.valueOf(),
- };
- const subject = createTimerange(300000, Aggregators.COUNT, timeframe);
- expect(subject.end - subject.start).toEqual(300000);
- });
- it('should return 6 minute range when given 6 minute timeframe', () => {
- const end = moment();
- const timeframe = {
- start: end.clone().subtract(6, 'minutes').valueOf(),
- end: end.valueOf(),
- };
- const subject = createTimerange(300000, Aggregators.COUNT, timeframe);
- expect(subject.end - subject.start).toEqual(360000);
- });
+ describe('Rate Aggs', () => {
+ it('should return a 20 second range for last 1 second', () => {
+ const subject = createTimerange(1000, Aggregators.RATE, timeframe);
+ expect(subject.end - subject.start).toEqual(1000 * 2);
+ });
+ it('should return a 5 minute range for last 1 minute', () => {
+ const subject = createTimerange(60000, Aggregators.RATE, timeframe);
+ expect(subject.end - subject.start).toEqual(60000 * 2);
+ });
+ it('should return 25 minute range for last 5 minutes', () => {
+ const subject = createTimerange(300000, Aggregators.RATE, timeframe);
+ expect(subject.end - subject.start).toEqual(300000 * 2);
+ });
+ it('should return 5 hour range for last hour', () => {
+ const subject = createTimerange(3600000, Aggregators.RATE, timeframe);
+ expect(subject.end - subject.start).toEqual(3600000 * 2);
});
- describe('Rate Aggs', () => {
- it('should return 8 minute range when given 4 minute timeframe', () => {
- const end = moment();
- const timeframe = {
- start: end.clone().subtract(4, 'minutes').valueOf(),
- end: end.valueOf(),
- };
- const subject = createTimerange(300000, Aggregators.RATE, timeframe);
- expect(subject.end - subject.start).toEqual(300000 * 2);
- });
- it('should return 12 minute range when given 6 minute timeframe', () => {
- const end = moment();
- const timeframe = {
- start: end.clone().subtract(6, 'minutes').valueOf(),
- end: end.valueOf(),
- };
- const subject = createTimerange(300000, Aggregators.RATE, timeframe);
- expect(subject.end - subject.start).toEqual(300000 * 2);
- });
+ it('should return a 5 day range for last day', () => {
+ const subject = createTimerange(86400000, Aggregators.RATE, timeframe);
+ expect(subject.end - subject.start).toEqual(86400000 * 2);
});
});
- describe('with partial timeframe', () => {
- describe('Basic Metric Aggs', () => {
- it('should return 5 minute range for last 5 minutes', () => {
- const end = moment();
- const timeframe = {
- end: end.valueOf(),
- };
- const subject = createTimerange(300000, Aggregators.AVERAGE, timeframe);
- expect(subject).toEqual({
- start: end.clone().subtract(5, 'minutes').valueOf(),
- end: end.valueOf(),
- });
- });
+ describe('With lastPeriodEnd', () => {
+ it('should return a minute and 1 second range for last 1 second when the lastPeriodEnd is less than the timeframe start', () => {
+ const subject = createTimerange(
+ 1000,
+ Aggregators.COUNT,
+ timeframe,
+ end.clone().subtract(1, 'minutes').valueOf()
+ );
+ expect(subject.end - subject.start).toEqual(61000);
});
- describe('Rate Aggs', () => {
- it('should return 10 minute range for last 5 minutes', () => {
- const end = moment();
- const timeframe = {
- end: end.valueOf(),
- };
- const subject = createTimerange(300000, Aggregators.RATE, timeframe);
- expect(subject).toEqual({
- start: end
- .clone()
- .subtract(300 * 2, 'seconds')
- .valueOf(),
- end: end.valueOf(),
- });
- });
+ it('should return a second range for last 1 second when the lastPeriodEnd is not less than the timeframe start', () => {
+ const subject = createTimerange(
+ 1000,
+ Aggregators.COUNT,
+ timeframe,
+ end.clone().add(2, 'seconds').valueOf()
+ );
+ expect(subject.end - subject.start).toEqual(1000);
});
});
});
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.ts
index 75f4dda7ff8d6..257318a0bd109 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_timerange.ts
@@ -11,21 +11,20 @@ import { Aggregators } from '../../../../../common/custom_threshold_rule/types';
export const createTimerange = (
interval: number,
aggType: Aggregators,
- timeframe?: { end: number; start?: number },
+ timeframe: { end: string; start: string },
lastPeriodEnd?: number
) => {
- const to = moment(timeframe ? timeframe.end : Date.now()).valueOf();
+ const end = moment(timeframe.end).valueOf();
+ let start = moment(timeframe.start).valueOf();
// Rate aggregations need 5 buckets worth of data
const minimumBuckets = aggType === Aggregators.RATE ? 2 : 1;
- const calculatedFrom = lastPeriodEnd ? lastPeriodEnd - interval : to - interval * minimumBuckets;
+ start = start - interval * minimumBuckets;
- // Use either the timeframe.start when the start is less then calculatedFrom
- // OR use the calculatedFrom
- const from =
- timeframe && timeframe.start && timeframe.start <= calculatedFrom
- ? timeframe.start
- : calculatedFrom;
+ // Use lastPeriodEnd - interval when it's less than start
+ if (lastPeriodEnd && lastPeriodEnd - interval < start) {
+ start = lastPeriodEnd - interval;
+ }
- return { start: from, end: to };
+ return { start, end };
};
diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts
index 33410cbfb9742..97523fc102c1a 100644
--- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts
+++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts
@@ -41,8 +41,8 @@ export const evaluateRule = async >> => {
const { criteria, groupBy, searchConfiguration } = params;
diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts
index 0837973ec7ee2..6be334dc62f2b 100644
--- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts
+++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts
@@ -97,6 +97,11 @@ function createFindResponse(sloList: SLO[]): SavedObjectsFindResponse
};
}
+function getTimeRange() {
+ const date = new Date(Date.now()).toISOString();
+ return { dateStart: date, dateEnd: date };
+}
+
describe('BurnRateRuleExecutor', () => {
let esClientMock: ElasticsearchClientMock;
let soClientMock: jest.Mocked;
@@ -178,6 +183,7 @@ describe('BurnRateRuleExecutor', () => {
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
})
).rejects.toThrowError();
});
@@ -198,6 +204,7 @@ describe('BurnRateRuleExecutor', () => {
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(esClientMock.search).not.toHaveBeenCalled();
@@ -246,6 +253,7 @@ describe('BurnRateRuleExecutor', () => {
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(alertWithLifecycleMock).not.toBeCalled();
@@ -291,6 +299,7 @@ describe('BurnRateRuleExecutor', () => {
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(alertWithLifecycleMock).not.toBeCalled();
@@ -339,6 +348,7 @@ describe('BurnRateRuleExecutor', () => {
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(alertWithLifecycleMock).toBeCalledWith({
@@ -436,6 +446,7 @@ describe('BurnRateRuleExecutor', () => {
spaceId: 'irrelevant',
state: {},
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(alertWithLifecycleMock).toBeCalledWith({
diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts
index 63c5ede6d7d6c..41ff9b53a97e2 100644
--- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts
+++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.ts
@@ -19,7 +19,7 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
import { upperCase } from 'lodash';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/server';
-import { ALL_VALUE } from '@kbn/slo-schema';
+import { ALL_VALUE, toDurationUnit } from '@kbn/slo-schema';
import { AlertsLocatorParams, getAlertUrl } from '../../../../common';
import {
SLO_ID_FIELD,
@@ -63,6 +63,7 @@ export const getRuleExecutor = ({
params,
startedAt,
spaceId,
+ getTimeRange,
}): ReturnType<
ExecutorType<
BurnRateRuleParams,
@@ -88,7 +89,22 @@ export const getRuleExecutor = ({
return { state: {} };
}
- const results = await evaluate(esClient.asCurrentUser, slo, params, startedAt);
+ const burnRateWindows = getBurnRateWindows(params.windows);
+ const longestLookbackWindow = burnRateWindows.reduce((acc, winDef) => {
+ return winDef.longDuration.isShorterThan(acc.longDuration) ? acc : winDef;
+ }, burnRateWindows[0]);
+ const { dateStart, dateEnd } = getTimeRange(
+ `${longestLookbackWindow.longDuration.value}${longestLookbackWindow.longDuration.unit}`
+ );
+
+ const results = await evaluate(
+ esClient.asCurrentUser,
+ slo,
+ params,
+ dateStart,
+ dateEnd,
+ burnRateWindows
+ );
if (results.length > 0) {
for (const result of results) {
@@ -196,6 +212,19 @@ export const getRuleExecutor = ({
return { state: {} };
};
+export function getBurnRateWindows(windows: WindowSchema[]) {
+ return windows.map((winDef) => {
+ return {
+ ...winDef,
+ longDuration: new Duration(winDef.longWindow.value, toDurationUnit(winDef.longWindow.unit)),
+ shortDuration: new Duration(
+ winDef.shortWindow.value,
+ toDurationUnit(winDef.shortWindow.unit)
+ ),
+ };
+ });
+}
+
function getActionGroupName(id: string) {
switch (id) {
case HIGH_PRIORITY_ACTION.id:
diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.test.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.test.ts
index 730fe8ae66e46..6a10733b13690 100644
--- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.test.ts
+++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.test.ts
@@ -8,8 +8,10 @@
import { createBurnRateRule } from '../fixtures/rule';
import { buildQuery } from './build_query';
import { createKQLCustomIndicator, createSLO } from '../../../../services/slo/fixtures/slo';
+import { getBurnRateWindows } from '../executor';
-const STARTED_AT = new Date('2023-01-01T00:00:00.000Z');
+const DATE_START = '2022-12-29T00:00:00.000Z';
+const DATE_END = '2023-01-01T00:00:00.000Z';
describe('buildQuery()', () => {
it('should return a valid query for occurrences', () => {
@@ -18,7 +20,8 @@ describe('buildQuery()', () => {
indicator: createKQLCustomIndicator(),
});
const rule = createBurnRateRule(slo);
- expect(buildQuery(STARTED_AT, slo, rule)).toMatchSnapshot();
+ const burnRateWindows = getBurnRateWindows(rule.windows);
+ expect(buildQuery(slo, DATE_START, DATE_END, burnRateWindows)).toMatchSnapshot();
});
it('should return a valid query with afterKey', () => {
const slo = createSLO({
@@ -26,7 +29,12 @@ describe('buildQuery()', () => {
indicator: createKQLCustomIndicator(),
});
const rule = createBurnRateRule(slo);
- expect(buildQuery(STARTED_AT, slo, rule, { instanceId: 'example' })).toMatchSnapshot();
+ const burnRateWindows = getBurnRateWindows(rule.windows);
+ expect(
+ buildQuery(slo, DATE_START, DATE_END, burnRateWindows, {
+ instanceId: 'example',
+ })
+ ).toMatchSnapshot();
});
it('should return a valid query for timeslices', () => {
const slo = createSLO({
@@ -35,6 +43,7 @@ describe('buildQuery()', () => {
budgetingMethod: 'timeslices',
});
const rule = createBurnRateRule(slo);
- expect(buildQuery(STARTED_AT, slo, rule)).toMatchSnapshot();
+ const burnRateWindows = getBurnRateWindows(rule.windows);
+ expect(buildQuery(slo, DATE_START, DATE_END, burnRateWindows)).toMatchSnapshot();
});
});
diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.ts
index 8d5bfe795aa08..fad8c1ef5d1d7 100644
--- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.ts
+++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/build_query.ts
@@ -7,10 +7,10 @@
import moment from 'moment';
import { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
-import { Duration, SLO, toDurationUnit, toMomentUnitOfTime } from '../../../../domain/models';
-import { BurnRateRuleParams, WindowSchema } from '../types';
+import { Duration, SLO, toMomentUnitOfTime } from '../../../../domain/models';
+import { WindowSchema } from '../types';
-type BurnRateWindowWithDuration = WindowSchema & {
+export type BurnRateWindowWithDuration = WindowSchema & {
longDuration: Duration;
shortDuration: Duration;
};
@@ -99,7 +99,11 @@ function buildWindowAgg(
};
}
-function buildWindowAggs(startedAt: Date, slo: SLO, burnRateWindows: BurnRateWindowWithDuration[]) {
+function buildWindowAggs(
+ startedAt: string,
+ slo: SLO,
+ burnRateWindows: BurnRateWindowWithDuration[]
+) {
return burnRateWindows.reduce((acc, winDef, index) => {
const shortDateRange = getLookbackDateRange(startedAt, winDef.shortDuration);
const longDateRange = getLookbackDateRange(startedAt, winDef.longDuration);
@@ -150,27 +154,12 @@ function buildEvaluation(burnRateWindows: BurnRateWindowWithDuration[]) {
}
export function buildQuery(
- startedAt: Date,
slo: SLO,
- params: BurnRateRuleParams,
+ dateStart: string,
+ dateEnd: string,
+ burnRateWindows: BurnRateWindowWithDuration[],
afterKey?: EvaluationAfterKey
) {
- const burnRateWindows = params.windows.map((winDef) => {
- return {
- ...winDef,
- longDuration: new Duration(winDef.longWindow.value, toDurationUnit(winDef.longWindow.unit)),
- shortDuration: new Duration(
- winDef.shortWindow.value,
- toDurationUnit(winDef.shortWindow.unit)
- ),
- };
- });
-
- const longestLookbackWindow = burnRateWindows.reduce((acc, winDef) => {
- return winDef.longDuration.isShorterThan(acc.longDuration) ? acc : winDef;
- }, burnRateWindows[0]);
- const longestDateRange = getLookbackDateRange(startedAt, longestLookbackWindow.longDuration);
-
return {
size: 0,
query: {
@@ -181,8 +170,8 @@ export function buildQuery(
{
range: {
'@timestamp': {
- gte: longestDateRange.from.toISOString(),
- lt: longestDateRange.to.toISOString(),
+ gte: dateStart,
+ lt: dateEnd,
},
},
},
@@ -197,7 +186,7 @@ export function buildQuery(
sources: [{ instanceId: { terms: { field: 'slo.instanceId' } } }],
},
aggs: {
- ...buildWindowAggs(startedAt, slo, burnRateWindows),
+ ...buildWindowAggs(dateEnd, slo, burnRateWindows),
...buildEvaluation(burnRateWindows),
},
},
@@ -205,9 +194,9 @@ export function buildQuery(
};
}
-function getLookbackDateRange(startedAt: Date, duration: Duration): { from: Date; to: Date } {
+function getLookbackDateRange(startedAt: string, duration: Duration): { from: Date; to: Date } {
const unit = toMomentUnitOfTime(duration.unit);
- const now = moment(startedAt).startOf('minute');
+ const now = moment(startedAt);
const from = now.clone().subtract(duration.value, unit);
const to = now.clone();
diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/evaluate.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/evaluate.ts
index 8461382fc1564..cdd408297f137 100644
--- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/evaluate.ts
+++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/lib/evaluate.ts
@@ -12,6 +12,7 @@ import { BurnRateRuleParams } from '../types';
import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../assets/constants';
import {
buildQuery,
+ BurnRateWindowWithDuration,
EvaluationAfterKey,
generateAboveThresholdKey,
generateBurnRateKey,
@@ -65,12 +66,13 @@ export interface EvalutionAggResults {
async function queryAllResults(
esClient: ElasticsearchClient,
slo: SLO,
- params: BurnRateRuleParams,
- startedAt: Date,
+ dateStart: string,
+ dateEnd: string,
+ burnRateWindows: BurnRateWindowWithDuration[],
buckets: EvaluationBucket[] = [],
lastAfterKey?: { instanceId: string }
): Promise {
- const queryAndAggs = buildQuery(startedAt, slo, params, lastAfterKey);
+ const queryAndAggs = buildQuery(slo, dateStart, dateEnd, burnRateWindows, lastAfterKey);
const results = await esClient.search({
index: SLO_DESTINATION_INDEX_PATTERN,
...queryAndAggs,
@@ -84,8 +86,9 @@ async function queryAllResults(
return queryAllResults(
esClient,
slo,
- params,
- startedAt,
+ dateStart,
+ dateEnd,
+ burnRateWindows,
[...buckets, ...results.aggregations.instances.buckets],
results.aggregations.instances.after_key
);
@@ -95,9 +98,11 @@ export async function evaluate(
esClient: ElasticsearchClient,
slo: SLO,
params: BurnRateRuleParams,
- startedAt: Date
+ dateStart: string,
+ dateEnd: string,
+ burnRateWindows: BurnRateWindowWithDuration[]
) {
- const buckets = await queryAllResults(esClient, slo, params, startedAt);
+ const buckets = await queryAllResults(esClient, slo, dateStart, dateEnd, burnRateWindows);
return transformBucketToResults(buckets, params);
}
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
index 30d17ea1bfe06..58aa875cf2344 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
@@ -148,6 +148,10 @@ function createRule(shouldWriteAlerts: boolean = true) {
startedAt,
state,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = new Date(Date.now()).toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
})) ?? {}) as Record);
previousStartedAt = startedAt;
diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts
index 56002fa2d22bd..4ef589edadacb 100644
--- a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts
+++ b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts
@@ -96,4 +96,8 @@ export const createDefaultAlertExecutorOptions = <
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
...(maintenanceWindowIds ? { maintenanceWindowIds } : {}),
+ getTimeRange: () => {
+ const date = new Date(Date.now()).toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts
index a73561b7a34ff..0f0eeece6f8f6 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts
@@ -72,6 +72,10 @@ describe('legacyRules_notification_alert_type', () => {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = new Date('2019-12-14T16:40:33.400Z').toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
};
alert = legacyRulesNotificationAlertType({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts
index 6591ebdc5200f..bbefafb102423 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts
@@ -284,6 +284,10 @@ export const previewRulesRoute = async (
state: statePreview,
logger,
flappingSettings: DISABLE_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = startedAt.toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
})) as { state: TState });
const errors = loggedStatusChanges
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts
index 63f5024067cea..88f7049dcd261 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts
@@ -7,8 +7,8 @@
import { of } from 'rxjs';
import { CoreSetup } from '@kbn/core/server';
-import { executor, getSearchParams, getValidTimefieldSort, tryToParseAsDate } from './executor';
-import { ExecutorOptions, OnlyEsQueryRuleParams } from './types';
+import { executor, getValidTimefieldSort, tryToParseAsDate } from './executor';
+import { ExecutorOptions } from './types';
import { Comparator } from '../../../common/comparator_types';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { loggerMock } from '@kbn/logging-mocks';
@@ -117,6 +117,10 @@ describe('es_query executor', () => {
state: { latestTimestamp: undefined },
spaceId: 'default',
logger,
+ getTimeRange: () => {
+ const date = new Date(Date.now()).toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
} as unknown as ExecutorOptions;
it('should throw error for invalid comparator', async () => {
@@ -141,8 +145,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
});
await executor(coreMock, defaultExecutorOptions);
expect(mockFetchEsQuery).toHaveBeenCalledWith({
@@ -157,6 +159,8 @@ describe('es_query executor', () => {
scopedClusterClient: scopedClusterClientMock,
logger,
},
+ dateStart: new Date().toISOString(),
+ dateEnd: new Date().toISOString(),
});
expect(mockFetchSearchSourceQuery).not.toHaveBeenCalled();
});
@@ -173,8 +177,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
});
await executor(coreMock, {
...defaultExecutorOptions,
@@ -191,6 +193,8 @@ describe('es_query executor', () => {
share: undefined,
},
spacePrefix: '',
+ dateStart: new Date().toISOString(),
+ dateEnd: new Date().toISOString(),
});
expect(mockFetchEsQuery).not.toHaveBeenCalled();
});
@@ -207,8 +211,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
});
await executor(coreMock, {
...defaultExecutorOptions,
@@ -222,10 +224,11 @@ describe('es_query executor', () => {
scopedClusterClient: scopedClusterClientMock,
logger,
share: undefined,
- dataViews: undefined,
},
spacePrefix: '',
publicBaseUrl: 'https://localhost:5601',
+ dateStart: new Date().toISOString(),
+ dateEnd: new Date().toISOString(),
});
expect(mockFetchEsQuery).not.toHaveBeenCalled();
expect(mockFetchSearchSourceQuery).not.toHaveBeenCalled();
@@ -243,8 +246,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
});
await executor(coreMock, {
...defaultExecutorOptions,
@@ -269,8 +270,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
});
await executor(coreMock, {
@@ -343,8 +342,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
});
await executor(coreMock, {
@@ -491,8 +488,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
});
await executor(coreMock, {
@@ -568,8 +563,6 @@ describe('es_query executor', () => {
],
truncated: true,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
});
await executor(coreMock, {
...defaultExecutorOptions,
@@ -611,8 +604,6 @@ describe('es_query executor', () => {
],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
});
await executor(coreMock, {
@@ -673,8 +664,6 @@ describe('es_query executor', () => {
]);
mockFetchEsQuery.mockResolvedValueOnce({
parsedResults: { results: [], truncated: false },
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
});
await executor(coreMock, {
@@ -771,8 +760,6 @@ describe('es_query executor', () => {
results: [],
truncated: false,
},
- dateStart: new Date().toISOString(),
- dateEnd: new Date().toISOString(),
link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id',
});
await executor(coreMock, {
@@ -848,36 +835,4 @@ describe('es_query executor', () => {
expect(result).toEqual('2018-12-31T19:00:00.000Z');
});
});
-
- describe('getSearchParams', () => {
- it('should return search params correctly', () => {
- const result = getSearchParams(defaultProps as OnlyEsQueryRuleParams);
- expect(result.parsedQuery.query).toBe('test-query');
- });
-
- it('should throw invalid query error', () => {
- expect(() =>
- getSearchParams({ ...defaultProps, esQuery: '' } as OnlyEsQueryRuleParams)
- ).toThrow('invalid query specified: "" - query must be JSON');
- });
-
- it('should throw invalid query error due to missing query property', () => {
- expect(() =>
- getSearchParams({
- ...defaultProps,
- esQuery: '{ "someProperty": "test-query" }',
- } as OnlyEsQueryRuleParams)
- ).toThrow('invalid query specified: "{ "someProperty": "test-query" }" - query must be JSON');
- });
-
- it('should throw invalid window size error', () => {
- expect(() =>
- getSearchParams({
- ...defaultProps,
- timeWindowSize: 5,
- timeWindowUnit: 'r',
- } as OnlyEsQueryRuleParams)
- ).toThrow('invalid format for windowSize: "5r"');
- });
- });
});
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts
index e6366caf53130..468872be4cd78 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts
@@ -7,7 +7,6 @@
import { sha256 } from 'js-sha256';
import { i18n } from '@kbn/i18n';
import { CoreSetup } from '@kbn/core/server';
-import { parseDuration } from '@kbn/alerting-plugin/server';
import { isGroupAggregation, UngroupedGroupId } from '@kbn/triggers-actions-ui-plugin/common';
import { ALERT_EVALUATION_VALUE, ALERT_REASON, ALERT_URL } from '@kbn/rule-data-utils';
@@ -41,6 +40,7 @@ export async function executor(core: CoreSetup, options: ExecutorOptions = {};
for (const result of parsedResults.results) {
@@ -208,53 +215,6 @@ export async function executor(core: CoreSetup, options: ExecutorOptions = []
): undefined | string {
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts
index 193f559ce2d62..46b3bb2ff495f 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts
@@ -10,7 +10,6 @@ import { Comparator } from '../../../../common/comparator_types';
import { fetchEsQuery } from './fetch_es_query';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { loggerMock } from '@kbn/logging-mocks';
-import { getSearchParams } from './get_search_params';
jest.mock('@kbn/triggers-actions-ui-plugin/common', () => {
const actual = jest.requireActual('@kbn/triggers-actions-ui-plugin/common');
@@ -56,7 +55,8 @@ describe('fetchEsQuery', () => {
};
it('should add time filter if timestamp if defined and excludeHitsFromPreviousRun is true', async () => {
const params = defaultParams;
- const { dateStart, dateEnd } = getSearchParams(params);
+ const date = new Date().toISOString();
+
await fetchEsQuery({
ruleId: 'abc',
name: 'test-rule',
@@ -65,6 +65,8 @@ describe('fetchEsQuery', () => {
services,
spacePrefix: '',
publicBaseUrl: '',
+ dateStart: date,
+ dateEnd: date,
});
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
{
@@ -116,8 +118,8 @@ describe('fetchEsQuery', () => {
range: {
'@timestamp': {
format: 'strict_date_optional_time',
- gte: dateStart,
- lte: dateEnd,
+ gte: date,
+ lte: date,
},
},
},
@@ -147,7 +149,8 @@ describe('fetchEsQuery', () => {
it('should not add time filter if timestamp is undefined', async () => {
const params = defaultParams;
- const { dateStart, dateEnd } = getSearchParams(params);
+ const date = new Date().toISOString();
+
await fetchEsQuery({
ruleId: 'abc',
name: 'test-rule',
@@ -156,6 +159,8 @@ describe('fetchEsQuery', () => {
services,
spacePrefix: '',
publicBaseUrl: '',
+ dateStart: date,
+ dateEnd: date,
});
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
{
@@ -181,8 +186,8 @@ describe('fetchEsQuery', () => {
range: {
'@timestamp': {
format: 'strict_date_optional_time',
- gte: dateStart,
- lte: dateEnd,
+ gte: date,
+ lte: date,
},
},
},
@@ -212,7 +217,8 @@ describe('fetchEsQuery', () => {
it('should not add time filter if excludeHitsFromPreviousRun is false', async () => {
const params = { ...defaultParams, excludeHitsFromPreviousRun: false };
- const { dateStart, dateEnd } = getSearchParams(params);
+ const date = new Date().toISOString();
+
await fetchEsQuery({
ruleId: 'abc',
name: 'test-rule',
@@ -221,6 +227,8 @@ describe('fetchEsQuery', () => {
services,
spacePrefix: '',
publicBaseUrl: '',
+ dateStart: date,
+ dateEnd: date,
});
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
{
@@ -246,8 +254,8 @@ describe('fetchEsQuery', () => {
range: {
'@timestamp': {
format: 'strict_date_optional_time',
- gte: dateStart,
- lte: dateEnd,
+ gte: date,
+ lte: date,
},
},
},
@@ -277,7 +285,8 @@ describe('fetchEsQuery', () => {
it('should set size: 0 and top hits size to size parameter if grouping alerts', async () => {
const params = { ...defaultParams, groupBy: 'top', termField: 'host.name', termSize: 10 };
- const { dateStart, dateEnd } = getSearchParams(params);
+ const date = new Date().toISOString();
+
await fetchEsQuery({
ruleId: 'abc',
name: 'test-rule',
@@ -286,6 +295,8 @@ describe('fetchEsQuery', () => {
services,
spacePrefix: '',
publicBaseUrl: '',
+ dateStart: date,
+ dateEnd: date,
});
expect(scopedClusterClientMock.asCurrentUser.search).toHaveBeenCalledWith(
{
@@ -338,8 +349,8 @@ describe('fetchEsQuery', () => {
range: {
'@timestamp': {
format: 'strict_date_optional_time',
- gte: dateStart,
- lte: dateEnd,
+ gte: date,
+ lte: date,
},
},
},
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
index f44ad3f470106..3d4f47575966c 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
@@ -16,7 +16,7 @@ import { ES_QUERY_ID } from '@kbn/rule-data-utils';
import { getComparatorScript } from '../../../../common';
import { OnlyEsQueryRuleParams } from '../types';
import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query';
-import { getSearchParams } from './get_search_params';
+import { getParsedQuery } from '../util';
export interface FetchEsQueryOpts {
ruleId: string;
@@ -30,6 +30,8 @@ export interface FetchEsQueryOpts {
logger: Logger;
};
alertLimit?: number;
+ dateStart: string;
+ dateEnd: string;
}
/**
@@ -44,17 +46,20 @@ export async function fetchEsQuery({
timestamp,
services,
alertLimit,
+ dateStart,
+ dateEnd,
}: FetchEsQueryOpts) {
const { scopedClusterClient, logger } = services;
const esClient = scopedClusterClient.asCurrentUser;
const isGroupAgg = isGroupAggregation(params.termField);
const isCountAgg = isCountAggregation(params.aggType);
const {
+ query,
+ fields,
// eslint-disable-next-line @typescript-eslint/naming-convention
- parsedQuery: { query, fields, runtime_mappings, _source },
- dateStart,
- dateEnd,
- } = getSearchParams(params);
+ runtime_mappings,
+ _source,
+ } = getParsedQuery(params);
const filter =
timestamp && params.excludeHitsFromPreviousRun
@@ -136,8 +141,6 @@ export async function fetchEsQuery({
esResult: searchResult,
resultLimit: alertLimit,
}),
- dateStart,
- dateEnd,
link,
};
}
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts
index 5a2e8d4d761c2..1d7096d20140e 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts
@@ -6,26 +6,15 @@
*/
import { OnlyEsqlQueryRuleParams } from '../types';
-import { stubbedSavedObjectIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub';
-import { DataView } from '@kbn/data-views-plugin/common';
-import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import { Comparator } from '../../../../common/comparator_types';
import { getEsqlQuery } from './fetch_esql_query';
-const createDataView = () => {
- const id = 'test-id';
- const {
- type,
- version,
- attributes: { timeFieldName, fields, title },
- } = stubbedSavedObjectIndexPattern(id);
+const getTimeRange = () => {
+ const date = Date.now();
+ const dateStart = new Date(date - 300000).toISOString();
+ const dateEnd = new Date(date).toISOString();
- return new DataView({
- spec: { id, type, version, timeFieldName, fields: JSON.parse(fields), title },
- fieldFormats: fieldFormatsMock,
- shortDotsEnable: false,
- metaFields: ['_id', '_type', '_score'],
- });
+ return { dateStart, dateEnd };
};
const defaultParams: OnlyEsqlQueryRuleParams = {
@@ -44,7 +33,6 @@ const defaultParams: OnlyEsqlQueryRuleParams = {
describe('fetchEsqlQuery', () => {
describe('getEsqlQuery', () => {
- const dataViewMock = createDataView();
afterAll(() => {
jest.resetAllMocks();
});
@@ -58,7 +46,8 @@ describe('fetchEsqlQuery', () => {
it('should generate the correct query', async () => {
const params = defaultParams;
- const { query, dateStart, dateEnd } = getEsqlQuery(dataViewMock, params, undefined);
+ const { dateStart, dateEnd } = getTimeRange();
+ const query = getEsqlQuery(params, undefined, dateStart, dateEnd);
expect(query).toMatchInlineSnapshot(`
Object {
@@ -80,13 +69,12 @@ describe('fetchEsqlQuery', () => {
"query": "from test",
}
`);
- expect(dateStart).toMatch('2020-02-09T23:10:41.941Z');
- expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z');
});
it('should generate the correct query with the alertLimit', async () => {
const params = defaultParams;
- const { query, dateStart, dateEnd } = getEsqlQuery(dataViewMock, params, 100);
+ const { dateStart, dateEnd } = getTimeRange();
+ const query = getEsqlQuery(params, 100, dateStart, dateEnd);
expect(query).toMatchInlineSnapshot(`
Object {
@@ -108,8 +96,6 @@ describe('fetchEsqlQuery', () => {
"query": "from test | limit 100",
}
`);
- expect(dateStart).toMatch('2020-02-09T23:10:41.941Z');
- expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z');
});
});
});
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts
index ae729e51703d4..ad806401a8fd1 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { DataView, DataViewsContract, getTime } from '@kbn/data-plugin/common';
import { parseAggregationResults } from '@kbn/triggers-actions-ui-plugin/common';
import { SharePluginStart } from '@kbn/share-plugin/server';
import { IScopedClusterClient, Logger } from '@kbn/core/server';
@@ -22,8 +21,9 @@ export interface FetchEsqlQueryOpts {
logger: Logger;
scopedClusterClient: IScopedClusterClient;
share: SharePluginStart;
- dataViews: DataViewsContract;
};
+ dateStart: string;
+ dateEnd: string;
}
export async function fetchEsqlQuery({
@@ -33,14 +33,12 @@ export async function fetchEsqlQuery({
services,
spacePrefix,
publicBaseUrl,
+ dateStart,
+ dateEnd,
}: FetchEsqlQueryOpts) {
- const { logger, scopedClusterClient, dataViews } = services;
+ const { logger, scopedClusterClient } = services;
const esClient = scopedClusterClient.asCurrentUser;
- const dataView = await dataViews.create({
- timeFieldName: params.timeField,
- });
-
- const { query, dateStart, dateEnd } = getEsqlQuery(dataView, params, alertLimit);
+ const query = getEsqlQuery(params, alertLimit, dateStart, dateEnd);
logger.debug(`ES|QL query rule (${ruleId}) query: ${JSON.stringify(query)}`);
@@ -66,23 +64,15 @@ export async function fetchEsqlQuery({
},
resultLimit: alertLimit,
}),
- dateStart,
- dateEnd,
};
}
export const getEsqlQuery = (
- dataView: DataView,
params: OnlyEsqlQueryRuleParams,
- alertLimit: number | undefined
+ alertLimit: number | undefined,
+ dateStart: string,
+ dateEnd: string
) => {
- const timeRange = {
- from: `now-${params.timeWindowSize}${params.timeWindowUnit}`,
- to: 'now',
- };
- const timerangeFilter = getTime(dataView, timeRange);
- const dateStart = timerangeFilter?.query.range[params.timeField].gte;
- const dateEnd = timerangeFilter?.query.range[params.timeField].lte;
const rangeFilter: unknown[] = [
{
range: {
@@ -103,9 +93,5 @@ export const getEsqlQuery = (
},
},
};
- return {
- query,
- dateStart,
- dateEnd,
- };
+ return query;
};
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
index 94a8ea648c1e9..a753c34908a20 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
@@ -32,6 +32,14 @@ const createDataView = () => {
});
};
+const getTimeRange = () => {
+ const date = Date.now();
+ const dateStart = new Date(date - 300000).toISOString();
+ const dateEnd = new Date(date).toISOString();
+
+ return { dateStart, dateEnd };
+};
+
const defaultParams: OnlySearchSourceRuleParams = {
size: 100,
timeWindowSize: 5,
@@ -65,11 +73,14 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
- const { searchSource, dateStart, dateEnd } = updateSearchSource(
+ const { dateStart, dateEnd } = getTimeRange();
+ const searchSource = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
- undefined
+ undefined,
+ dateStart,
+ dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
@@ -94,8 +105,6 @@ describe('fetchSearchSourceQuery', () => {
}
`);
expect(searchRequest.aggs).toMatchInlineSnapshot(`Object {}`);
- expect(dateStart).toMatch('2020-02-09T23:10:41.941Z');
- expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z');
});
it('with latest timestamp in between the given time range ', async () => {
@@ -103,11 +112,14 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
- const { searchSource } = updateSearchSource(
+ const { dateStart, dateEnd } = getTimeRange();
+ const searchSource = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
- '2020-02-09T23:12:41.941Z'
+ '2020-02-09T23:12:41.941Z',
+ dateStart,
+ dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
@@ -147,11 +159,14 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
- const { searchSource } = updateSearchSource(
+ const { dateStart, dateEnd } = getTimeRange();
+ const searchSource = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
- '2020-01-09T22:12:41.941Z'
+ '2020-01-09T22:12:41.941Z',
+ dateStart,
+ dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
@@ -183,11 +198,14 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
- const { searchSource } = updateSearchSource(
+ const { dateStart, dateEnd } = getTimeRange();
+ const searchSource = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
- '2020-02-09T23:12:41.941Z'
+ '2020-02-09T23:12:41.941Z',
+ dateStart,
+ dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
@@ -225,11 +243,14 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
- const { searchSource } = updateSearchSource(
+ const { dateStart, dateEnd } = getTimeRange();
+ const searchSource = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
- '2020-02-09T23:12:41.941Z'
+ '2020-02-09T23:12:41.941Z',
+ dateStart,
+ dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
expect(searchRequest.size).toMatchInlineSnapshot(`0`);
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
index dc8a4db2610d7..b0e8474d33b5b 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
@@ -9,7 +9,6 @@ import { buildRangeFilter, Filter } from '@kbn/es-query';
import {
DataView,
DataViewsContract,
- getTime,
ISearchSource,
ISearchStartSearchSource,
SortDirection,
@@ -40,6 +39,8 @@ export interface FetchSearchSourceQueryOpts {
share: SharePluginStart;
dataViews: DataViewsContract;
};
+ dateStart: string;
+ dateEnd: string;
}
export async function fetchSearchSourceQuery({
@@ -49,6 +50,8 @@ export async function fetchSearchSourceQuery({
latestTimestamp,
spacePrefix,
services,
+ dateStart,
+ dateEnd,
}: FetchSearchSourceQueryOpts) {
const { logger, searchSourceClient } = services;
const isGroupAgg = isGroupAggregation(params.termField);
@@ -57,11 +60,13 @@ export async function fetchSearchSourceQuery({
const initialSearchSource = await searchSourceClient.create(params.searchConfiguration);
const index = initialSearchSource.getField('index') as DataView;
- const { searchSource, dateStart, dateEnd } = updateSearchSource(
+ const searchSource = updateSearchSource(
initialSearchSource,
index,
params,
latestTimestamp,
+ dateStart,
+ dateEnd,
alertLimit
);
@@ -87,8 +92,6 @@ export async function fetchSearchSourceQuery({
numMatches: Number(searchResult.hits.total),
searchResult,
parsedResults: parseAggregationResults({ isCountAgg, isGroupAgg, esResult: searchResult }),
- dateStart,
- dateEnd,
};
}
@@ -97,6 +100,8 @@ export function updateSearchSource(
index: DataView,
params: OnlySearchSourceRuleParams,
latestTimestamp: string | undefined,
+ dateStart: string,
+ dateEnd: string,
alertLimit?: number
) {
const isGroupAgg = isGroupAggregation(params.termField);
@@ -108,20 +113,19 @@ export function updateSearchSource(
searchSource.setField('size', isGroupAgg ? 0 : params.size);
- const timeRange = {
- from: `now-${params.timeWindowSize}${params.timeWindowUnit}`,
- to: 'now',
- };
- const timerangeFilter = getTime(index, timeRange);
- const dateStart = timerangeFilter?.query.range[timeFieldName].gte;
- const dateEnd = timerangeFilter?.query.range[timeFieldName].lte;
- const filters = [timerangeFilter];
+ const field = index.fields.find((f) => f.name === timeFieldName);
+ const filters = [
+ buildRangeFilter(
+ field!,
+ { lte: dateEnd, gte: dateStart, format: 'strict_date_optional_time' },
+ index
+ ),
+ ];
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, format: 'strict_date_optional_time' },
@@ -159,11 +163,7 @@ export function updateSearchSource(
...(isGroupAgg ? { topHitsSize: params.size } : {}),
})
);
- return {
- searchSource: searchSourceChild,
- dateStart,
- dateEnd,
- };
+ return searchSourceChild;
}
async function generateLink(
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/get_search_params.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/get_search_params.ts
deleted file mode 100644
index 126ddb3009287..0000000000000
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/get_search_params.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-import { i18n } from '@kbn/i18n';
-import { parseDuration } from '@kbn/alerting-plugin/common';
-import { OnlyEsQueryRuleParams } from '../types';
-
-export function getSearchParams(queryParams: OnlyEsQueryRuleParams) {
- const date = Date.now();
- const { esQuery, timeWindowSize, timeWindowUnit } = queryParams;
-
- let parsedQuery;
- try {
- parsedQuery = JSON.parse(esQuery);
- } catch (err) {
- throw new Error(getInvalidQueryError(esQuery));
- }
-
- if (parsedQuery && !parsedQuery.query) {
- throw new Error(getInvalidQueryError(esQuery));
- }
-
- const window = `${timeWindowSize}${timeWindowUnit}`;
- let timeWindow: number;
- try {
- timeWindow = parseDuration(window);
- } catch (err) {
- throw new Error(getInvalidWindowSizeError(window));
- }
-
- const dateStart = new Date(date - timeWindow).toISOString();
- const dateEnd = new Date(date).toISOString();
-
- return { parsedQuery, dateStart, dateEnd };
-}
-
-function getInvalidWindowSizeError(windowValue: string) {
- return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', {
- defaultMessage: 'invalid format for windowSize: "{windowValue}"',
- values: {
- windowValue,
- },
- });
-}
-
-function getInvalidQueryError(query: string) {
- return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', {
- defaultMessage: 'invalid query specified: "{query}" - query must be JSON',
- values: {
- query,
- },
- });
-}
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts
index 374f932177995..440ed0a21c266 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts
@@ -10,8 +10,6 @@ import type { Writable } from '@kbn/utility-types';
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { loggingSystemMock } from '@kbn/core/server/mocks';
-import type { DataViewSpec } from '@kbn/data-views-plugin/common';
-import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { getRuleType } from './rule_type';
import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params';
import { ActionContext } from './action_context';
@@ -566,31 +564,32 @@ describe('ruleType', () => {
});
describe('search source query', () => {
- const dataViewMock = createStubDataView({
- spec: {
- id: 'test-id',
- title: 'test-title',
- timeFieldName: 'time-field',
- fields: {
- message: {
- name: 'message',
- type: 'string',
- scripted: false,
- searchable: false,
- aggregatable: false,
- readFromDocValues: false,
- },
- timestamp: {
- name: 'timestamp',
- type: 'date',
- scripted: false,
- searchable: true,
- aggregatable: false,
- readFromDocValues: false,
- },
+ const dataViewMock = {
+ id: 'test-id',
+ title: 'test-title',
+ timeFieldName: 'timestamp',
+ fields: [
+ {
+ name: 'message',
+ type: 'string',
+ displayName: 'message',
+ scripted: false,
+ filterable: false,
+ aggregatable: false,
},
+ {
+ name: 'timestamp',
+ type: 'date',
+ displayName: 'timestamp',
+ scripted: false,
+ filterable: false,
+ aggregatable: false,
+ },
+ ],
+ toSpec: () => {
+ return { id: 'test-id', title: 'test-title', timeFieldName: 'timestamp', fields: [] };
},
- });
+ };
const defaultParams: OnlySearchSourceRuleParams = {
size: 100,
timeWindowSize: 5,
@@ -632,9 +631,11 @@ describe('ruleType', () => {
const searchResult: ESSearchResponse = generateResults([]);
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
- (ruleServices.dataViews.create as jest.Mock).mockImplementationOnce((spec: DataViewSpec) =>
- createStubDataView({ spec })
- );
+ (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
+ ...dataViewMock.toSpec(),
+ toSpec: () => dataViewMock.toSpec(),
+ toMinimalSpec: () => dataViewMock.toSpec(),
+ });
(searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => {
if (name === 'index') {
return dataViewMock;
@@ -669,9 +670,11 @@ describe('ruleType', () => {
const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] };
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
- (ruleServices.dataViews.create as jest.Mock).mockImplementationOnce((spec: DataViewSpec) =>
- createStubDataView({ spec })
- );
+ (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
+ ...dataViewMock.toSpec(),
+ toSpec: () => dataViewMock.toSpec(),
+ toMinimalSpec: () => dataViewMock.toSpec(),
+ });
(searchSourceInstanceMock.getField as jest.Mock).mockImplementation((name: string) => {
if (name === 'index') {
return dataViewMock;
@@ -711,32 +714,6 @@ describe('ruleType', () => {
});
describe('ESQL query', () => {
- const dataViewMock = {
- id: 'test-id',
- title: 'test-title',
- timeFieldName: 'time-field',
- fields: [
- {
- name: 'message',
- type: 'string',
- displayName: 'message',
- scripted: false,
- filterable: false,
- aggregatable: false,
- },
- {
- name: 'timestamp',
- type: 'date',
- displayName: 'timestamp',
- scripted: false,
- filterable: false,
- aggregatable: false,
- },
- ],
- toSpec: () => {
- return { id: 'test-id', title: 'test-title', timeFieldName: 'timestamp', fields: [] };
- },
- };
const defaultParams: OnlyEsqlQueryRuleParams = {
size: 100,
timeWindowSize: 5,
@@ -777,12 +754,6 @@ describe('ruleType', () => {
it('rule executor handles no documents returned by ES', async () => {
const params = defaultParams;
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
-
- (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
- ...dataViewMock.toSpec(),
- toSpec: () => dataViewMock.toSpec(),
- });
-
const searchResult = {
columns: [
{ name: 'timestamp', type: 'date' },
@@ -801,12 +772,6 @@ describe('ruleType', () => {
it('rule executor schedule actions when condition met', async () => {
const params = defaultParams;
const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
-
- (ruleServices.dataViews.create as jest.Mock).mockResolvedValueOnce({
- ...dataViewMock.toSpec(),
- toSpec: () => dataViewMock.toSpec(),
- });
-
const searchResult = {
columns: [
{ name: 'timestamp', type: 'date' },
@@ -932,5 +897,9 @@ async function invokeExecutor({
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange: () => {
+ const date = new Date(Date.now()).toISOString();
+ return { dateStart: date, dateEnd: date };
+ },
});
}
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.test.ts
new file mode 100644
index 0000000000000..33277947abaab
--- /dev/null
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.test.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { OnlyEsQueryRuleParams } from './types';
+import { Comparator } from '../../../common/comparator_types';
+import { getParsedQuery } from './util';
+
+describe('es_query utils', () => {
+ const defaultProps = {
+ size: 3,
+ timeWindowSize: 5,
+ timeWindowUnit: 'm',
+ threshold: [],
+ thresholdComparator: '>=' as Comparator,
+ esQuery: '{ "query": "test-query" }',
+ index: ['test-index'],
+ timeField: '',
+ searchType: 'esQuery',
+ excludeHitsFromPreviousRun: true,
+ aggType: 'count',
+ groupBy: 'all',
+ searchConfiguration: {},
+ esqlQuery: { esql: 'test-query' },
+ };
+
+ describe('getParsedQuery', () => {
+ it('should return search params correctly', () => {
+ const parsedQuery = getParsedQuery(defaultProps as OnlyEsQueryRuleParams);
+ expect(parsedQuery.query).toBe('test-query');
+ });
+
+ it('should throw invalid query error', () => {
+ expect(() =>
+ getParsedQuery({ ...defaultProps, esQuery: '' } as OnlyEsQueryRuleParams)
+ ).toThrow('invalid query specified: "" - query must be JSON');
+ });
+
+ it('should throw invalid query error due to missing query property', () => {
+ expect(() =>
+ getParsedQuery({
+ ...defaultProps,
+ esQuery: '{ "someProperty": "test-query" }',
+ } as OnlyEsQueryRuleParams)
+ ).toThrow('invalid query specified: "{ "someProperty": "test-query" }" - query must be JSON');
+ });
+ });
+});
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts
index d10218fea7d4f..7dd3c23a0715c 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/util.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { i18n } from '@kbn/i18n';
+import { OnlyEsQueryRuleParams } from './types';
import { EsQueryRuleParams } from './rule_type_params';
export function isEsQueryRule(searchType: EsQueryRuleParams['searchType']) {
@@ -18,3 +20,29 @@ export function isSearchSourceRule(searchType: EsQueryRuleParams['searchType'])
export function isEsqlQueryRule(searchType: EsQueryRuleParams['searchType']) {
return searchType === 'esqlQuery';
}
+
+export function getParsedQuery(queryParams: OnlyEsQueryRuleParams) {
+ const { esQuery } = queryParams;
+
+ let parsedQuery;
+ try {
+ parsedQuery = JSON.parse(esQuery);
+ } catch (err) {
+ throw new Error(getInvalidQueryError(esQuery));
+ }
+
+ if (parsedQuery && !parsedQuery.query) {
+ throw new Error(getInvalidQueryError(esQuery));
+ }
+
+ return parsedQuery;
+}
+
+function getInvalidQueryError(query: string) {
+ return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', {
+ defaultMessage: 'invalid query specified: "{query}" - query must be JSON',
+ values: {
+ query,
+ },
+ });
+}
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts
index 0d0c7a9f49b33..def4f2eadd8da 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts
@@ -20,6 +20,11 @@ import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_set
let fakeTimer: sinon.SinonFakeTimers;
+function getTimeRange() {
+ const date = new Date(Date.now()).toISOString();
+ return { dateStart: date, dateEnd: date };
+}
+
describe('ruleType', () => {
const logger = loggingSystemMock.create().get();
const data = {
@@ -224,6 +229,7 @@ describe('ruleType', () => {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(alertServices.alertsClient.report).toHaveBeenCalledWith({
@@ -318,6 +324,7 @@ describe('ruleType', () => {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled();
@@ -386,6 +393,7 @@ describe('ruleType', () => {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled();
@@ -453,6 +461,7 @@ describe('ruleType', () => {
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
+ getTimeRange,
});
expect(data.timeSeriesQuery).toHaveBeenCalledWith(
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts
index e4411ad50638a..4bf1a2e2accd3 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts
@@ -219,6 +219,7 @@ export function getRuleType(
services,
params,
logger,
+ getTimeRange,
} = options;
const { alertsClient, scopedClusterClient } = services;
@@ -237,7 +238,8 @@ export function getRuleType(
}
const esClient = scopedClusterClient.asCurrentUser;
- const date = new Date().toISOString();
+ const { dateStart, dateEnd } = getTimeRange(`${params.timeWindowSize}${params.timeWindowUnit}`);
+
// the undefined values below are for config-schema optional types
const queryParams: TimeSeriesQuery = {
index: params.index,
@@ -247,8 +249,8 @@ export function getRuleType(
groupBy: params.groupBy,
termField: params.termField,
termSize: params.termSize,
- dateStart: date,
- dateEnd: date,
+ dateStart,
+ dateEnd,
timeWindowSize: params.timeWindowSize,
timeWindowUnit: params.timeWindowUnit,
interval: undefined,
@@ -269,6 +271,7 @@ export function getRuleType(
TIME_SERIES_BUCKET_SELECTOR_FIELD
),
},
+ useCalculatedDateRange: false,
});
logger.debug(`rule ${ID}:${ruleId} "${name}" query result: ${JSON.stringify(result)}`);
@@ -309,7 +312,7 @@ export function getRuleType(
)} ${params.threshold.join(' and ')}`;
const baseContext: BaseActionContext = {
- date,
+ date: dateEnd,
group: alertId,
value,
conditions: humanFn,
@@ -338,7 +341,7 @@ export function getRuleType(
const alertId = recoveredAlert.getId();
logger.debug(`setting context for recovered alert ${alertId}`);
const baseContext: BaseActionContext = {
- date,
+ date: dateEnd,
value: unmetGroupValues[alertId] ?? 'unknown',
group: alertId,
conditions: `${agg} is NOT ${getHumanReadableComparator(
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index ec61fc696fd77..3f6db08f7b178 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -35063,7 +35063,6 @@
"xpack.stackAlerts.esQuery.invalidQueryErrorMessage": "recherche spécifiée non valide : \"{query}\" - la recherche doit être au format JSON",
"xpack.stackAlerts.esQuery.invalidTermSizeMaximumErrorMessage": "[termSize] : doit être inférieure ou égale à {maxGroups}",
"xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage": "[threshold] : requiert deux éléments pour le comparateur \"{thresholdComparator}\"",
- "xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage": "format non valide pour windowSize : \"{windowValue}\"",
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "La recherche correspondait à {count} documents dans le/la/les dernier(s)/dernière(s) {window}.",
"xpack.stackAlerts.esQuery.ui.queryError": "Erreur lors du test de la recherche : {message}",
"xpack.stackAlerts.esQuery.ui.testQueryGroupedResponse": "La recherche groupée correspondait à {groups} groupes dans le/la/les dernier(s)/dernière(s) {window}.",
@@ -37791,7 +37790,6 @@
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "Détectez les alertes qui passent rapidement de l'état actif à l'état récupéré et réduisez le bruit non souhaité de ces alertes bagotantes.",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "Désactivé",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "Activé (recommandé)",
- "xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError": "Impossible de récupérer les paramètres des règles.",
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "Enregistrer",
"xpack.triggersActionsUI.rulesSettings.modal.title": "Paramètres de règle",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "Impossible de mettre à jour les paramètres des règles.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index ecf2b3716ddec..3e2165795ded9 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -35062,7 +35062,6 @@
"xpack.stackAlerts.esQuery.invalidQueryErrorMessage": "無効なクエリが指定されました: \"{query}\" - クエリはJSONでなければなりません",
"xpack.stackAlerts.esQuery.invalidTermSizeMaximumErrorMessage": "[termSize]:{maxGroups}以下でなければなりません",
"xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage": "[threshold]:「{thresholdComparator}」比較子の場合には2つの要素が必要です",
- "xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage": "windowSizeの無効な形式:\"{windowValue}\"",
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "前回の{window}でクエリが{count}個のドキュメントと一致しました。",
"xpack.stackAlerts.esQuery.ui.queryError": "クエリのテストエラー:{message}",
"xpack.stackAlerts.esQuery.ui.testQueryGroupedResponse": "グループ化されたクエリは、直近の{window}件に{groups}グループと一致しました。",
@@ -37782,7 +37781,6 @@
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "アクティブと回復済みの状態がすばやく切り替わるアラートを検出し、これらのフラップアラートに対する不要なノイズを低減します。",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "オフ",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "オン(推奨)",
- "xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError": "ルール設定を取得できませんでした。",
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "保存",
"xpack.triggersActionsUI.rulesSettings.modal.title": "ルール設定",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "ルール設定を更新できませんでした。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 65c962b7db7f1..6172231f9a2d1 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -35056,7 +35056,6 @@
"xpack.stackAlerts.esQuery.invalidQueryErrorMessage": "指定的查询无效:“{query}”- 查询必须为 JSON",
"xpack.stackAlerts.esQuery.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}",
"xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素",
- "xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage": "windowSize 的格式无效:“{windowValue}”",
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "查询在过去 {window} 匹配 {count} 个文档。",
"xpack.stackAlerts.esQuery.ui.queryError": "测试查询时出错:{message}",
"xpack.stackAlerts.esQuery.ui.testQueryGroupedResponse": "过去 {window} 与 {groups} 个组匹配的分组查询。",
@@ -37776,7 +37775,6 @@
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "检测在“活动”和“已恢复”状态之间快速切换的告警,并为这些摆动告警减少不必要噪音。",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "关闭",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "开(建议)",
- "xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError": "无法获取规则设置。",
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "保存",
"xpack.triggersActionsUI.rulesSettings.modal.title": "规则设置",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "无法更新规则设置。",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_flapping_form_section.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx
similarity index 77%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_flapping_form_section.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx
index ef915eae6869b..cc7ef6c629fc2 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_flapping_form_section.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx
@@ -8,19 +8,7 @@
import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormRow,
- EuiFormRowProps,
- EuiIconTip,
- EuiRange,
- EuiRangeProps,
- EuiSpacer,
- EuiTitle,
- EuiText,
- EuiPanel,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText, EuiPanel } from '@elastic/eui';
import {
RulesSettingsFlappingProperties,
MIN_LOOK_BACK_WINDOW,
@@ -28,7 +16,7 @@ import {
MAX_LOOK_BACK_WINDOW,
MAX_STATUS_CHANGE_THRESHOLD,
} from '@kbn/alerting-plugin/common';
-import { useKibana } from '../../../common/lib/kibana';
+import { RulesSettingsRange } from '../rules_settings_range';
type OnChangeKey = keyof Omit;
@@ -81,16 +69,6 @@ const getStatusChangeThresholdRuleRuns = (amount: number) => {
);
};
-export interface RulesSettingsRangeProps {
- label: EuiFormRowProps['label'];
- labelPopoverText?: string;
- min: number;
- max: number;
- value: number;
- disabled?: EuiRangeProps['disabled'];
- onChange?: EuiRangeProps['onChange'];
-}
-
export const RulesSettingsFlappingTitle = () => {
return (
@@ -115,58 +93,19 @@ export const RulesSettingsFlappingDescription = () => {
);
};
-export const RulesSettingsRange = memo((props: RulesSettingsRangeProps) => {
- const { label, labelPopoverText, min, max, value, disabled, onChange, ...rest } = props;
-
- const renderLabel = () => {
- return (
-
- {label}
-
-
-
- );
- };
-
- return (
-
-
-
- );
-});
-
export interface RulesSettingsFlappingFormSectionProps {
flappingSettings: RulesSettingsFlappingProperties;
compressed?: boolean;
onChange: (key: OnChangeKey, value: number) => void;
+ canWrite: boolean;
}
export const RulesSettingsFlappingFormSection = memo(
(props: RulesSettingsFlappingFormSectionProps) => {
- const { flappingSettings, compressed = false, onChange } = props;
+ const { flappingSettings, compressed = false, onChange, canWrite } = props;
const { lookBackWindow, statusChangeThreshold } = flappingSettings;
- const {
- application: { capabilities },
- } = useKibana().services;
-
- const {
- rulesSettings: { writeFlappingSettingsUI },
- } = capabilities;
-
- const canWriteFlappingSettings = writeFlappingSettingsUI;
-
return (
{compressed && (
@@ -193,7 +132,7 @@ export const RulesSettingsFlappingFormSection = memo(
onChange={(e) => onChange('lookBackWindow', parseInt(e.currentTarget.value, 10))}
label={lookBackWindowLabel}
labelPopoverText={lookBackWindowHelp}
- disabled={!canWriteFlappingSettings}
+ disabled={!canWrite}
/>
@@ -205,7 +144,7 @@ export const RulesSettingsFlappingFormSection = memo(
onChange={(e) => onChange('statusChangeThreshold', parseInt(e.currentTarget.value, 10))}
label={statusChangeThresholdLabel}
labelPopoverText={statusChangeThresholdHelp}
- disabled={!canWriteFlappingSettings}
+ disabled={!canWrite}
/>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_section.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_section.tsx
new file mode 100644
index 0000000000000..a6e2f282d8894
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_section.tsx
@@ -0,0 +1,185 @@
+/*
+ * 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, { memo } from 'react';
+import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiForm,
+ EuiSpacer,
+ EuiSwitch,
+ EuiSwitchProps,
+ EuiPanel,
+ EuiText,
+ EuiEmptyPrompt,
+} from '@elastic/eui';
+import {
+ RulesSettingsFlappingFormSection,
+ RulesSettingsFlappingFormSectionProps,
+ RulesSettingsFlappingTitle,
+} from './rules_settings_flapping_form_section';
+
+const flappingDescription = i18n.translate(
+ 'xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription',
+ {
+ defaultMessage:
+ 'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts.',
+ }
+);
+
+const flappingOnLabel = i18n.translate(
+ 'xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel',
+ {
+ defaultMessage: 'On (recommended)',
+ }
+);
+
+const flappingOffLabel = i18n.translate(
+ 'xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel',
+ {
+ defaultMessage: 'Off',
+ }
+);
+
+export const RulesSettingsFlappingErrorPrompt = memo(() => {
+ return (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ );
+});
+
+interface RulesSettingsFlappingFormLeftProps {
+ settings: RulesSettingsFlappingProperties;
+ onChange: EuiSwitchProps['onChange'];
+ isSwitchDisabled: boolean;
+}
+
+export const RulesSettingsFlappingFormLeft = memo((props: RulesSettingsFlappingFormLeftProps) => {
+ const { settings, onChange, isSwitchDisabled } = props;
+
+ return (
+
+
+
+
+ {flappingDescription}
+
+
+
+
+
+
+
+ );
+});
+
+interface RulesSettingsFlappingFormRightProps {
+ settings: RulesSettingsFlappingProperties;
+ onChange: RulesSettingsFlappingFormSectionProps['onChange'];
+ canWrite: boolean;
+}
+
+export const RulesSettingsFlappingFormRight = memo((props: RulesSettingsFlappingFormRightProps) => {
+ const { settings, onChange, canWrite } = props;
+
+ if (!settings) {
+ return null;
+ }
+ if (!settings.enabled) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+});
+
+export interface RulesSettingsFlappingSectionProps {
+ onChange: (key: keyof RulesSettingsFlappingProperties, value: number | boolean) => void;
+ settings: RulesSettingsFlappingProperties;
+ canShow: boolean | Readonly<{ [x: string]: boolean }>;
+ canWrite: boolean;
+ hasError: boolean;
+}
+
+export const RulesSettingsFlappingSection = memo((props: RulesSettingsFlappingSectionProps) => {
+ const { onChange, settings, hasError, canShow, canWrite } = props;
+
+ if (!canShow) {
+ return null;
+ }
+ if (hasError) {
+ return ;
+ }
+ return (
+
+
+
+
+
+
+
+
+ onChange('enabled', e.target.checked)}
+ />
+ onChange(key, value)}
+ canWrite={canWrite}
+ />
+
+
+ );
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/query_delay/rules_settings_query_delay_section.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/query_delay/rules_settings_query_delay_section.tsx
new file mode 100644
index 0000000000000..468774fed6a29
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/query_delay/rules_settings_query_delay_section.tsx
@@ -0,0 +1,123 @@
+/*
+ * 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, { memo } from 'react';
+import {
+ MAX_QUERY_DELAY,
+ MIN_QUERY_DELAY,
+ RulesSettingsQueryDelayProperties,
+} from '@kbn/alerting-plugin/common';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiForm,
+ EuiSpacer,
+ EuiText,
+ EuiEmptyPrompt,
+ EuiTitle,
+} from '@elastic/eui';
+import { RulesSettingsRange } from '../rules_settings_range';
+
+const queryDelayDescription = i18n.translate(
+ 'xpack.triggersActionsUI.rulesSettings.modal.queryDelayDescription',
+ {
+ defaultMessage:
+ 'Delay all rule queries to mitigate the impact of index refresh intervals on data availability.',
+ }
+);
+
+const queryDelayLabel = i18n.translate('xpack.triggersActionsUI.rulesSettings.queryDelayLabel', {
+ defaultMessage: 'Query delay length (seconds)',
+});
+
+export const RulesSettingsQueryDelayErrorPrompt = memo(() => {
+ return (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ );
+});
+
+export const RulesSettingsQueryDelayTitle = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export interface RulesSettingsQueryDelaySectionProps {
+ onChange: (key: keyof RulesSettingsQueryDelayProperties, value: number | boolean) => void;
+ settings: RulesSettingsQueryDelayProperties;
+ canShow: boolean | Readonly<{ [x: string]: boolean }>;
+ canWrite: boolean;
+ hasError: boolean;
+}
+
+export const RulesSettingsQueryDelaySection = memo((props: RulesSettingsQueryDelaySectionProps) => {
+ const { onChange, settings, hasError, canShow, canWrite } = props;
+
+ if (!canShow) {
+ return null;
+ }
+ if (hasError) {
+ return ;
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+ {queryDelayDescription}
+
+
+
+ onChange('delay', parseInt(e.currentTarget.value, 10))}
+ label={queryDelayLabel}
+ disabled={!canWrite}
+ />
+
+
+
+ );
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.stories.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.stories.tsx
index 9bfe33e1ef887..f5da133df373e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.stories.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.stories.tsx
@@ -36,6 +36,8 @@ withAllPermission.decorators = [
save: true,
readFlappingSettingsUI: true,
writeFlappingSettingsUI: true,
+ readQueryDelaySettingsUI: true,
+ writeQueryDelaySettingsUI: true,
},
}),
}}
@@ -58,6 +60,8 @@ withReadPermission.decorators = [
save: false,
readFlappingSettingsUI: true,
writeFlappingSettingsUI: false,
+ readQueryDelaySettingsUI: true,
+ writeQueryDelaySettingsUI: false,
},
}),
}}
@@ -80,6 +84,8 @@ withNoPermission.decorators = [
save: false,
readFlappingSettingsUI: false,
writeFlappingSettingsUI: false,
+ readQueryDelaySettingsUI: false,
+ writeQueryDelaySettingsUI: false,
},
}),
}}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx
index 17cc4ea8bca8f..77c779318696e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx
@@ -11,18 +11,18 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render, cleanup, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { coreMock } from '@kbn/core/public/mocks';
-import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
+import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
import { RulesSettingsLink } from './rules_settings_link';
import { useKibana } from '../../../common/lib/kibana';
import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings';
-import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings';
+import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings';
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
getFlappingSettings: jest.fn(),
}));
-jest.mock('../../lib/rule_api/update_flapping_settings', () => ({
- updateFlappingSettings: jest.fn(),
+jest.mock('../../lib/rule_api/get_query_delay_settings', () => ({
+ getQueryDelaySettings: jest.fn(),
}));
const queryClient = new QueryClient({
@@ -41,8 +41,8 @@ const mocks = coreMock.createSetup();
const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction<
typeof getFlappingSettings
>;
-const updateFlappingSettingsMock = updateFlappingSettings as unknown as jest.MockedFunction<
- typeof updateFlappingSettings
+const getQueryDelaySettingsMock = getQueryDelaySettings as unknown as jest.MockedFunction<
+ typeof getQueryDelaySettings
>;
const mockFlappingSetting: RulesSettingsFlapping = {
@@ -54,6 +54,13 @@ const mockFlappingSetting: RulesSettingsFlapping = {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
+const mockQueryDelaySetting: RulesSettingsQueryDelay = {
+ delay: 10,
+ createdBy: 'test user',
+ updatedBy: 'test user',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+};
const RulesSettingsLinkWithProviders: React.FunctionComponent<{}> = () => (
@@ -77,10 +84,12 @@ describe('rules_settings_link', () => {
show: true,
writeFlappingSettingsUI: true,
readFlappingSettingsUI: true,
+ writeQueryDelaySettingsUI: true,
+ readQueryDelaySettingsUI: true,
},
};
getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
- updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
+ getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
});
afterEach(() => {
@@ -98,6 +107,58 @@ describe('rules_settings_link', () => {
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
});
+ test('renders the rules setting link correctly (readFlappingSettingsUI = true)', async () => {
+ const [
+ {
+ application: { capabilities },
+ },
+ ] = await mocks.getStartServices();
+ useKibanaMock().services.application.capabilities = {
+ ...capabilities,
+ rulesSettings: {
+ save: true,
+ show: true,
+ writeFlappingSettingsUI: true,
+ readFlappingSettingsUI: true,
+ writeQueryDelaySettingsUI: true,
+ readQueryDelaySettingsUI: false,
+ },
+ };
+
+ const result = render();
+ await waitFor(() => {
+ expect(result.getByText('Settings')).toBeInTheDocument();
+ });
+ expect(result.getByText('Settings')).not.toBeDisabled();
+ expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
+ });
+
+ test('renders the rules setting link correctly (readQueryDelaySettingsUI = true)', async () => {
+ const [
+ {
+ application: { capabilities },
+ },
+ ] = await mocks.getStartServices();
+ useKibanaMock().services.application.capabilities = {
+ ...capabilities,
+ rulesSettings: {
+ save: true,
+ show: true,
+ writeFlappingSettingsUI: true,
+ readFlappingSettingsUI: false,
+ writeQueryDelaySettingsUI: true,
+ readQueryDelaySettingsUI: true,
+ },
+ };
+
+ const result = render();
+ await waitFor(() => {
+ expect(result.getByText('Settings')).toBeInTheDocument();
+ });
+ expect(result.getByText('Settings')).not.toBeDisabled();
+ expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
+ });
+
test('clicking the settings link opens the rules settings modal', async () => {
const result = render();
await waitFor(() => {
@@ -124,6 +185,8 @@ describe('rules_settings_link', () => {
show: false,
writeFlappingSettingsUI: true,
readFlappingSettingsUI: true,
+ writeQueryDelaySettingsUI: true,
+ readQueryDelaySettingsUI: true,
},
};
@@ -139,6 +202,8 @@ describe('rules_settings_link', () => {
show: true,
writeFlappingSettingsUI: true,
readFlappingSettingsUI: false,
+ writeQueryDelaySettingsUI: true,
+ readQueryDelaySettingsUI: false,
},
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.tsx
index be3752dfc6fe6..06f00f392d68d 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.tsx
@@ -17,9 +17,9 @@ export const RulesSettingsLink = () => {
application: { capabilities },
} = useKibana().services;
- const { show, readFlappingSettingsUI } = capabilities.rulesSettings;
+ const { show, readFlappingSettingsUI, readQueryDelaySettingsUI } = capabilities.rulesSettings;
- if (!show || !readFlappingSettingsUI) {
+ if (!show || (!readFlappingSettingsUI && !readQueryDelaySettingsUI)) {
return null;
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx
index 6915a46123a40..54de45d909beb 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx
@@ -12,11 +12,13 @@ import { render, fireEvent, cleanup, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { coreMock } from '@kbn/core/public/mocks';
import { IToasts } from '@kbn/core/public';
-import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
+import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
import { RulesSettingsModal, RulesSettingsModalProps } from './rules_settings_modal';
import { useKibana } from '../../../common/lib/kibana';
import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings';
import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings';
+import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings';
+import { updateQueryDelaySettings } from '../../lib/rule_api/update_query_delay_settings';
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
@@ -25,6 +27,12 @@ jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
jest.mock('../../lib/rule_api/update_flapping_settings', () => ({
updateFlappingSettings: jest.fn(),
}));
+jest.mock('../../lib/rule_api/get_query_delay_settings', () => ({
+ getQueryDelaySettings: jest.fn(),
+}));
+jest.mock('../../lib/rule_api/update_query_delay_settings', () => ({
+ updateQueryDelaySettings: jest.fn(),
+}));
const queryClient = new QueryClient({
defaultOptions: {
@@ -45,6 +53,12 @@ const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFun
const updateFlappingSettingsMock = updateFlappingSettings as unknown as jest.MockedFunction<
typeof updateFlappingSettings
>;
+const getQueryDelaySettingsMock = getQueryDelaySettings as unknown as jest.MockedFunction<
+ typeof getQueryDelaySettings
+>;
+const updateQueryDelaySettingsMock = updateQueryDelaySettings as unknown as jest.MockedFunction<
+ typeof updateQueryDelaySettings
+>;
const mockFlappingSetting: RulesSettingsFlapping = {
enabled: true,
@@ -55,6 +69,13 @@ const mockFlappingSetting: RulesSettingsFlapping = {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
+const mockQueryDelaySetting: RulesSettingsQueryDelay = {
+ delay: 10,
+ createdBy: 'test user',
+ updatedBy: 'test user',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+};
const modalProps: RulesSettingsModalProps = {
isVisible: true,
@@ -87,6 +108,8 @@ describe('rules_settings_modal', () => {
show: true,
writeFlappingSettingsUI: true,
readFlappingSettingsUI: true,
+ writeQueryDelaySettingsUI: true,
+ readQueryDelaySettingsUI: true,
},
};
@@ -99,6 +122,8 @@ describe('rules_settings_modal', () => {
getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
+ getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
+ updateQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
});
afterEach(() => {
@@ -113,9 +138,9 @@ describe('rules_settings_modal', () => {
await waitFor(() => {
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
});
- expect(result.getByTestId('rulesSettingsModalEnableSwitch').getAttribute('aria-checked')).toBe(
- 'true'
- );
+ expect(
+ result.getByTestId('rulesSettingsFlappingEnableSwitch').getAttribute('aria-checked')
+ ).toBe('true');
expect(result.getByTestId('lookBackWindowRangeInput').getAttribute('value')).toBe('10');
expect(result.getByTestId('statusChangeThresholdRangeInput').getAttribute('value')).toBe('10');
@@ -190,6 +215,15 @@ describe('rules_settings_modal', () => {
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
});
+ const lookBackWindowInput = result.getByTestId('lookBackWindowRangeInput');
+ const statusChangeThresholdInput = result.getByTestId('statusChangeThresholdRangeInput');
+
+ fireEvent.change(lookBackWindowInput, { target: { value: 20 } });
+ fireEvent.change(statusChangeThresholdInput, { target: { value: 5 } });
+
+ expect(lookBackWindowInput.getAttribute('value')).toBe('20');
+ expect(statusChangeThresholdInput.getAttribute('value')).toBe('5');
+
// Try saving
userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
await waitFor(() => {
@@ -207,9 +241,9 @@ describe('rules_settings_modal', () => {
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
});
- expect(result.queryByTestId('rulesSettingsModalFlappingOffPrompt')).toBe(null);
- userEvent.click(result.getByTestId('rulesSettingsModalEnableSwitch'));
- expect(result.queryByTestId('rulesSettingsModalFlappingOffPrompt')).not.toBe(null);
+ expect(result.queryByTestId('rulesSettingsFlappingOffPrompt')).toBe(null);
+ userEvent.click(result.getByTestId('rulesSettingsFlappingEnableSwitch'));
+ expect(result.queryByTestId('rulesSettingsFlappingOffPrompt')).not.toBe(null);
});
test('form elements are disabled when provided with insufficient write permissions', async () => {
@@ -232,7 +266,7 @@ describe('rules_settings_modal', () => {
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
});
- expect(result.getByTestId('rulesSettingsModalEnableSwitch')).toBeDisabled();
+ expect(result.getByTestId('rulesSettingsFlappingEnableSwitch')).toBeDisabled();
expect(result.getByTestId('lookBackWindowRangeInput')).toBeDisabled();
expect(result.getByTestId('statusChangeThresholdRangeInput')).toBeDisabled();
expect(result.getByTestId('rulesSettingsModalSaveButton')).toBeDisabled();
@@ -259,6 +293,118 @@ describe('rules_settings_modal', () => {
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
});
- expect(result.getByTestId('rulesSettingsErrorPrompt')).toBeInTheDocument();
+ expect(result.queryByTestId('rulesSettingsFlappingSection')).toBe(null);
+ });
+
+ test('renders query delay settings correctly', async () => {
+ const result = render();
+ expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1);
+ await waitFor(() => {
+ expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
+ });
+ expect(result.getByTestId('queryDelayRangeInput').getAttribute('value')).toBe('10');
+
+ expect(result.getByTestId('rulesSettingsModalCancelButton')).toBeInTheDocument();
+ expect(result.getByTestId('rulesSettingsModalSaveButton').getAttribute('disabled')).toBeFalsy();
+ });
+
+ test('can save query delay settings', async () => {
+ const result = render();
+ await waitFor(() => {
+ expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
+ });
+
+ const queryDelayRangeInput = result.getByTestId('queryDelayRangeInput');
+ fireEvent.change(queryDelayRangeInput, { target: { value: 20 } });
+ expect(queryDelayRangeInput.getAttribute('value')).toBe('20');
+
+ // Try saving
+ userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
+
+ await waitFor(() => {
+ expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
+ });
+ expect(modalProps.onClose).toHaveBeenCalledTimes(1);
+ expect(updateQueryDelaySettingsMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ queryDelaySettings: {
+ delay: 20,
+ },
+ })
+ );
+ expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1);
+ expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
+ expect(modalProps.onSave).toHaveBeenCalledTimes(1);
+ });
+
+ test('handles errors when saving query delay settings', async () => {
+ updateQueryDelaySettingsMock.mockRejectedValue('failed!');
+
+ const result = render();
+ await waitFor(() => {
+ expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
+ });
+
+ const queryDelayRangeInput = result.getByTestId('queryDelayRangeInput');
+ fireEvent.change(queryDelayRangeInput, { target: { value: 20 } });
+ expect(queryDelayRangeInput.getAttribute('value')).toBe('20');
+
+ // Try saving
+ userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
+ await waitFor(() => {
+ expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
+ });
+ expect(modalProps.onClose).toHaveBeenCalledTimes(1);
+ expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1);
+ expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
+ expect(modalProps.onSave).toHaveBeenCalledTimes(1);
+ });
+
+ test('query delay form elements are disabled when provided with insufficient write permissions', async () => {
+ const [
+ {
+ application: { capabilities },
+ },
+ ] = await mocks.getStartServices();
+ useKibanaMock().services.application.capabilities = {
+ ...capabilities,
+ rulesSettings: {
+ save: true,
+ show: true,
+ writeQueryDelaySettingsUI: false,
+ readQueryDelaySettingsUI: true,
+ },
+ };
+ const result = render();
+ await waitFor(() => {
+ expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
+ });
+
+ expect(result.getByTestId('queryDelayRangeInput')).toBeDisabled();
+ expect(result.getByTestId('rulesSettingsModalSaveButton')).toBeDisabled();
+ });
+
+ test('query delay form elements are not visible when provided with insufficient read permissions', async () => {
+ const [
+ {
+ application: { capabilities },
+ },
+ ] = await mocks.getStartServices();
+ useKibanaMock().services.application.capabilities = {
+ ...capabilities,
+ rulesSettings: {
+ save: true,
+ show: false,
+ writeQueryDelaySettingsUI: true,
+ readQueryDelaySettingsUI: false,
+ },
+ };
+
+ const result = render();
+ await waitFor(() => {
+ expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
+ });
+
+ expect(result.queryByTestId('rulesSettingsQueryDelaySection')).toBe(null);
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx
index d71a134fa0f79..c15286325495a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx
@@ -6,7 +6,11 @@
*/
import React, { memo, useState } from 'react';
-import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common';
+import {
+ RulesSettingsFlappingProperties,
+ RulesSettingsProperties,
+ RulesSettingsQueryDelayProperties,
+} from '@kbn/alerting-plugin/common';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@@ -14,53 +18,22 @@ import {
EuiButtonEmpty,
EuiCallOut,
EuiHorizontalRule,
- EuiFlexGroup,
- EuiFlexItem,
- EuiForm,
EuiModal,
EuiModalHeader,
EuiModalBody,
EuiModalFooter,
EuiModalHeaderTitle,
EuiSpacer,
- EuiSwitch,
- EuiSwitchProps,
- EuiPanel,
- EuiText,
EuiEmptyPrompt,
} from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
-import {
- RulesSettingsFlappingFormSection,
- RulesSettingsFlappingFormSectionProps,
- RulesSettingsFlappingTitle,
-} from './rules_settings_flapping_form_section';
import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings';
-import { useUpdateFlappingSettings } from '../../hooks/use_update_flapping_settings';
+import { RulesSettingsFlappingSection } from './flapping/rules_settings_flapping_section';
+import { RulesSettingsQueryDelaySection } from './query_delay/rules_settings_query_delay_section';
+import { useGetQueryDelaySettings } from '../../hooks/use_get_query_delay_settings';
+import { useUpdateRuleSettings } from '../../hooks/use_update_rules_settings';
import { CenterJustifiedSpinner } from '../center_justified_spinner';
-const flappingDescription = i18n.translate(
- 'xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription',
- {
- defaultMessage:
- 'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts.',
- }
-);
-
-const flappingOnLabel = i18n.translate(
- 'xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel',
- {
- defaultMessage: 'On (recommended)',
- }
-);
-
-const flappingOffLabel = i18n.translate(
- 'xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel',
- {
- defaultMessage: 'Off',
- }
-);
-
export const RulesSettingsErrorPrompt = memo(() => {
return (
{
);
});
-interface RulesSettingsModalFormLeftProps {
- settings: RulesSettingsFlappingProperties;
- onChange: EuiSwitchProps['onChange'];
- isSwitchDisabled: boolean;
-}
-
-export const RulesSettingsModalFormLeft = memo((props: RulesSettingsModalFormLeftProps) => {
- const { settings, onChange, isSwitchDisabled } = props;
-
- return (
-
-
-
-
- {flappingDescription}
-
-
-
-
-
-
-
- );
-});
-
-interface RulesSettingsModalFormRightProps {
- settings: RulesSettingsFlappingProperties;
- onChange: RulesSettingsFlappingFormSectionProps['onChange'];
-}
-
-export const RulesSettingsModalFormRight = memo((props: RulesSettingsModalFormRightProps) => {
- const { settings, onChange } = props;
-
- if (!settings) {
- return null;
- }
- if (!settings.enabled) {
- return (
-
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
- );
-});
-
export interface RulesSettingsModalProps {
isVisible: boolean;
setUpdatingRulesSettings?: (isUpdating: boolean) => void;
@@ -165,16 +74,27 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
application: { capabilities },
} = useKibana().services;
const {
- rulesSettings: { show, save, writeFlappingSettingsUI, readFlappingSettingsUI },
+ rulesSettings: {
+ show,
+ save,
+ writeFlappingSettingsUI,
+ readFlappingSettingsUI,
+ writeQueryDelaySettingsUI,
+ readQueryDelaySettingsUI,
+ },
} = capabilities;
- const [settings, setSettings] = useState();
+ const [flappingSettings, setFlappingSettings] = useState();
+ const [hasFlappingChanged, setHasFlappingChanged] = useState(false);
+
+ const [queryDelaySettings, setQueryDelaySettings] = useState();
+ const [hasQueryDelayChanged, setHasQueryDelayChanged] = useState(false);
- const { isLoading, isError: hasError } = useGetFlappingSettings({
+ const { isLoading: isFlappingLoading, isError: hasFlappingError } = useGetFlappingSettings({
enabled: isVisible,
onSuccess: (fetchedSettings) => {
- if (!settings) {
- setSettings({
+ if (!flappingSettings) {
+ setFlappingSettings({
enabled: fetchedSettings.enabled,
lookBackWindow: fetchedSettings.lookBackWindow,
statusChangeThreshold: fetchedSettings.statusChangeThreshold,
@@ -183,7 +103,18 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
},
});
- const { mutate } = useUpdateFlappingSettings({
+ const { isLoading: isQueryDelayLoading, isError: hasQueryDelayError } = useGetQueryDelaySettings({
+ enabled: isVisible,
+ onSuccess: (fetchedSettings) => {
+ if (!queryDelaySettings) {
+ setQueryDelaySettings({
+ delay: fetchedSettings.delay,
+ });
+ }
+ },
+ });
+
+ const { mutate } = useUpdateRuleSettings({
onSave,
onClose,
setUpdatingRulesSettings,
@@ -192,36 +123,56 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
// In the future when we have more settings sub-features, we should
// disassociate the rule settings capabilities (save, show) from the
// sub-feature capabilities (writeXSettingsUI).
- const canWriteFlappingSettings = save && writeFlappingSettingsUI && !hasError;
+ const canWriteFlappingSettings = save && writeFlappingSettingsUI && !hasFlappingError;
const canShowFlappingSettings = show && readFlappingSettingsUI;
+ const canWriteQueryDelaySettings = save && writeQueryDelaySettingsUI && !hasQueryDelayError;
+ const canShowQueryDelaySettings = show && readQueryDelaySettingsUI;
const handleSettingsChange = (
- key: keyof RulesSettingsFlappingProperties,
- value: number | boolean
+ setting: keyof RulesSettingsProperties,
+ key: keyof RulesSettingsFlappingProperties | keyof RulesSettingsQueryDelayProperties,
+ value: boolean | number
) => {
- if (!settings) {
- return;
+ if (setting === 'flapping') {
+ if (!flappingSettings) {
+ return;
+ }
+ const newSettings = {
+ ...flappingSettings,
+ [key]: value,
+ };
+ setFlappingSettings({
+ ...newSettings,
+ statusChangeThreshold: Math.min(
+ newSettings.lookBackWindow,
+ newSettings.statusChangeThreshold
+ ),
+ });
+ setHasFlappingChanged(true);
}
- const newSettings = {
- ...settings,
- [key]: value,
- };
-
- setSettings({
- ...newSettings,
- statusChangeThreshold: Math.min(
- newSettings.lookBackWindow,
- newSettings.statusChangeThreshold
- ),
- });
+ if (setting === 'queryDelay') {
+ if (!queryDelaySettings) {
+ return;
+ }
+ const newSettings = {
+ ...queryDelaySettings,
+ [key]: value,
+ };
+ setQueryDelaySettings(newSettings);
+ setHasQueryDelayChanged(true);
+ }
};
const handleSave = () => {
- if (!settings) {
- return;
+ const updatedSettings: RulesSettingsProperties = {};
+ if (canWriteFlappingSettings && hasFlappingChanged) {
+ updatedSettings.flapping = flappingSettings;
+ }
+ if (canWriteQueryDelaySettings && hasQueryDelayChanged) {
+ updatedSettings.queryDelay = queryDelaySettings;
}
- mutate(settings);
+ mutate(updatedSettings);
};
if (!isVisible) {
@@ -229,32 +180,36 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
}
const maybeRenderForm = () => {
- if (hasError || !canShowFlappingSettings) {
+ if (!canShowFlappingSettings && !canShowQueryDelaySettings) {
return ;
}
- if (!settings || isLoading) {
+ if (isFlappingLoading || isQueryDelayLoading) {
return ;
}
return (
-
-
-
-
-
-
-
-
- handleSettingsChange('enabled', e.target.checked)}
+ <>
+ {flappingSettings && (
+ handleSettingsChange('flapping', key, value)}
+ settings={flappingSettings}
+ canWrite={canWriteFlappingSettings}
+ canShow={canShowFlappingSettings}
+ hasError={hasFlappingError}
/>
- handleSettingsChange(key, value)}
- />
-
-
+ )}
+ {queryDelaySettings && (
+ <>
+
+ handleSettingsChange('queryDelay', key, value)}
+ settings={queryDelaySettings}
+ canWrite={canWriteQueryDelaySettings}
+ canShow={canShowQueryDelaySettings}
+ hasError={hasQueryDelayError}
+ />
+ >
+ )}
+ >
);
};
@@ -291,7 +246,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
fill
data-test-subj="rulesSettingsModalSaveButton"
onClick={handleSave}
- disabled={!canWriteFlappingSettings}
+ disabled={!canWriteFlappingSettings && !canWriteQueryDelaySettings}
>
{
+ const { label, labelPopoverText, min, max, value, disabled, onChange, ...rest } = props;
+
+ const renderLabel = () => {
+ return (
+
+ {label}
+
+ {labelPopoverText && (
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+
+ );
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts
index e3166999df9ed..f5c66decad481 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
import { useQuery } from '@tanstack/react-query';
import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
import { useKibana } from '../../common/lib/kibana';
@@ -18,27 +17,15 @@ interface UseGetFlappingSettingsProps {
export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => {
const { enabled, onSuccess } = props;
- const {
- http,
- notifications: { toasts },
- } = useKibana().services;
+ const { http } = useKibana().services;
const queryFn = () => {
return getFlappingSettings({ http });
};
- const onErrorFn = () => {
- toasts.addDanger(
- i18n.translate('xpack.triggersActionsUI.rulesSettings.modal.getRulesSettingsError', {
- defaultMessage: 'Failed to get rules Settings.',
- })
- );
- };
-
const { data, isFetching, isError, isLoadingError, isLoading } = useQuery({
queryKey: ['getFlappingSettings'],
queryFn,
- onError: onErrorFn,
onSuccess,
enabled,
refetchOnWindowFocus: false,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx
new file mode 100644
index 0000000000000..bf792e29ed604
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_setting.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 from 'react';
+import { renderHook } from '@testing-library/react-hooks/dom';
+import { waitFor } from '@testing-library/dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { useGetQueryDelaySettings } from './use_get_query_delay_settings';
+
+jest.mock('../lib/rule_api/get_query_delay_settings', () => ({
+ getQueryDelaySettings: jest.fn(),
+}));
+
+const { getQueryDelaySettings } = jest.requireMock('../lib/rule_api/get_query_delay_settings');
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ cacheTime: 0,
+ },
+ },
+});
+const wrapper = ({ children }: { children: Node }) => (
+ {children}
+);
+
+describe('useGetQueryDelaySettings', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call getQueryDelaySettings', async () => {
+ renderHook(() => useGetQueryDelaySettings({ enabled: true, onSuccess: () => {} }), {
+ wrapper,
+ });
+
+ await waitFor(() => {
+ expect(getQueryDelaySettings).toHaveBeenCalled();
+ });
+ });
+
+ it('should return isError = true if api fails', async () => {
+ getQueryDelaySettings.mockRejectedValue('This is an error.');
+
+ const { result } = renderHook(
+ () => useGetQueryDelaySettings({ enabled: true, onSuccess: () => {} }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_settings.ts
new file mode 100644
index 0000000000000..2956d380aa8cf
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_query_delay_settings.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { useQuery } from '@tanstack/react-query';
+import { RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
+import { useKibana } from '../../common/lib/kibana';
+import { getQueryDelaySettings } from '../lib/rule_api/get_query_delay_settings';
+
+interface UseGetQueryDelaySettingsProps {
+ enabled: boolean;
+ onSuccess: (settings: RulesSettingsQueryDelay) => void;
+}
+
+export const useGetQueryDelaySettings = (props: UseGetQueryDelaySettingsProps) => {
+ const { enabled, onSuccess } = props;
+ const { http } = useKibana().services;
+
+ const queryFn = () => {
+ return getQueryDelaySettings({ http });
+ };
+
+ const { data, isFetching, isError, isLoadingError, isLoading } = useQuery({
+ queryKey: ['getQueryDelaySettings'],
+ queryFn,
+ onSuccess,
+ enabled,
+ refetchOnWindowFocus: false,
+ retry: false,
+ });
+
+ return {
+ isLoading: isLoading || isFetching,
+ isError: isError || isLoadingError,
+ data,
+ };
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx
new file mode 100644
index 0000000000000..6a36a32ed2522
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.test.tsx
@@ -0,0 +1,109 @@
+/*
+ * 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 from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { act, renderHook } from '@testing-library/react-hooks/dom';
+import { waitFor } from '@testing-library/dom';
+import { useUpdateRuleSettings } from './use_update_rules_settings';
+
+const mockAddDanger = jest.fn();
+const mockAddSuccess = jest.fn();
+
+jest.mock('../../common/lib/kibana', () => {
+ const originalModule = jest.requireActual('../../common/lib/kibana');
+ return {
+ ...originalModule,
+ useKibana: () => {
+ const { services } = originalModule.useKibana();
+ return {
+ services: {
+ ...services,
+ notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } },
+ },
+ };
+ },
+ };
+});
+jest.mock('../lib/rule_api/update_query_delay_settings', () => ({
+ updateQueryDelaySettings: jest.fn(),
+}));
+jest.mock('../lib/rule_api/update_flapping_settings', () => ({
+ updateFlappingSettings: jest.fn(),
+}));
+
+const { updateQueryDelaySettings } = jest.requireMock(
+ '../lib/rule_api/update_query_delay_settings'
+);
+const { updateFlappingSettings } = jest.requireMock('../lib/rule_api/update_flapping_settings');
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ cacheTime: 0,
+ },
+ },
+});
+const wrapper = ({ children }: { children: Node }) => (
+ {children}
+);
+
+describe('useUpdateRuleSettings', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call onSuccess if api succeeds', async () => {
+ const { result } = renderHook(
+ () =>
+ useUpdateRuleSettings({
+ onSave: () => {},
+ onClose: () => {},
+ setUpdatingRulesSettings: () => {},
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ await act(async () => {
+ await result.current.mutate({
+ flapping: { enabled: true, lookBackWindow: 3, statusChangeThreshold: 3 },
+ queryDelay: { delay: 2 },
+ });
+ });
+ await waitFor(() =>
+ expect(mockAddSuccess).toBeCalledWith('Rules settings updated successfully.')
+ );
+ });
+
+ it('should call onError if api fails', async () => {
+ updateQueryDelaySettings.mockRejectedValue('');
+ updateFlappingSettings.mockRejectedValue('');
+
+ const { result } = renderHook(
+ () =>
+ useUpdateRuleSettings({
+ onSave: () => {},
+ onClose: () => {},
+ setUpdatingRulesSettings: () => {},
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ await act(async () => {
+ await result.current.mutate({
+ flapping: { enabled: true, lookBackWindow: 3, statusChangeThreshold: 3 },
+ queryDelay: { delay: 2 },
+ });
+ });
+
+ await waitFor(() => expect(mockAddDanger).toBeCalledWith('Failed to update rules settings.'));
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.ts
similarity index 68%
rename from x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_flapping_settings.ts
rename to x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.ts
index ee1309a3f6582..ee07dd5c914ab 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_flapping_settings.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_update_rules_settings.ts
@@ -7,17 +7,18 @@
import { i18n } from '@kbn/i18n';
import { useMutation } from '@tanstack/react-query';
-import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common';
+import { RulesSettingsProperties } from '@kbn/alerting-plugin/common';
import { useKibana } from '../../common/lib/kibana';
import { updateFlappingSettings } from '../lib/rule_api/update_flapping_settings';
+import { updateQueryDelaySettings } from '../lib/rule_api/update_query_delay_settings';
-interface UseUpdateFlappingSettingsProps {
+interface UseUpdateRuleSettingsProps {
onClose: () => void;
onSave?: () => void;
setUpdatingRulesSettings?: (isUpdating: boolean) => void;
}
-export const useUpdateFlappingSettings = (props: UseUpdateFlappingSettingsProps) => {
+export const useUpdateRuleSettings = (props: UseUpdateRuleSettingsProps) => {
const { onSave, onClose, setUpdatingRulesSettings } = props;
const {
@@ -25,8 +26,17 @@ export const useUpdateFlappingSettings = (props: UseUpdateFlappingSettingsProps)
notifications: { toasts },
} = useKibana().services;
- const mutationFn = (flappingSettings: RulesSettingsFlappingProperties) => {
- return updateFlappingSettings({ http, flappingSettings });
+ const mutationFn = async (settings: RulesSettingsProperties) => {
+ const updates = [];
+ if (settings.flapping) {
+ updates.push(updateFlappingSettings({ http, flappingSettings: settings.flapping }));
+ }
+
+ if (settings.queryDelay) {
+ updates.push(updateQueryDelaySettings({ http, queryDelaySettings: settings.queryDelay }));
+ }
+
+ return await Promise.all(updates);
};
return useMutation({
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.test.ts
new file mode 100644
index 0000000000000..0cf03ac8f4a50
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.test.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { httpServiceMock } from '@kbn/core/public/mocks';
+import { getQueryDelaySettings } from './get_query_delay_settings';
+
+const http = httpServiceMock.createStartContract();
+
+beforeEach(() => jest.resetAllMocks());
+
+describe('getQueryDelaySettings', () => {
+ test('should call get query delay settings api', async () => {
+ const apiResponse = {
+ delay: 10,
+ };
+ http.get.mockResolvedValueOnce(apiResponse);
+
+ const result = await getQueryDelaySettings({ http });
+ expect(result).toEqual({ delay: 10 });
+ expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "/internal/alerting/rules/settings/_query_delay",
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.ts
new file mode 100644
index 0000000000000..c65e447dbb99f
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_query_delay_settings.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { HttpSetup } from '@kbn/core/public';
+import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
+import { RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
+import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
+
+const rewriteBodyRes: RewriteRequestCase = ({ ...rest }: any) => ({
+ ...rest,
+});
+
+export const getQueryDelaySettings = async ({ http }: { http: HttpSetup }) => {
+ const res = await http.get>(
+ `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`
+ );
+ return rewriteBodyRes(res);
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.test.ts
new file mode 100644
index 0000000000000..78c0e5b89d3b5
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.test.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { httpServiceMock } from '@kbn/core/public/mocks';
+import { updateQueryDelaySettings } from './update_query_delay_settings';
+
+const http = httpServiceMock.createStartContract();
+
+beforeEach(() => jest.resetAllMocks());
+
+describe('updateQueryDelaySettings', () => {
+ test('should call update query delay settings api', async () => {
+ const apiResponse = {
+ delay: 10,
+ };
+ http.post.mockResolvedValueOnce(apiResponse);
+
+ const result = await updateQueryDelaySettings({ http, queryDelaySettings: { delay: 10 } });
+ expect(result).toEqual({ delay: 10 });
+ expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "/internal/alerting/rules/settings/_query_delay",
+ Object {
+ "body": "{\\"delay\\":10}",
+ },
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.ts
new file mode 100644
index 0000000000000..5affa3cabb000
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_query_delay_settings.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { HttpSetup } from '@kbn/core/public';
+import {
+ RulesSettingsQueryDelay,
+ RulesSettingsQueryDelayProperties,
+} from '@kbn/alerting-plugin/common';
+import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
+import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
+
+const rewriteBodyRes: RewriteRequestCase = ({ ...rest }: any) => ({
+ ...rest,
+});
+
+export const updateQueryDelaySettings = async ({
+ http,
+ queryDelaySettings,
+}: {
+ http: HttpSetup;
+ queryDelaySettings: RulesSettingsQueryDelayProperties;
+}) => {
+ let body: string;
+ try {
+ body = JSON.stringify({
+ delay: queryDelaySettings.delay,
+ });
+ } catch (e) {
+ throw new Error(`Unable to parse query delay settings update params: ${e}`);
+ }
+
+ const res = await http.post>(
+ `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_query_delay`,
+ {
+ body,
+ }
+ );
+
+ return rewriteBodyRes(res);
+};
diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts
index 514601612db21..0b86ce6b30487 100644
--- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts
@@ -713,6 +713,28 @@ describe('timeSeriesQuery', () => {
{ ignore: [404], meta: true }
);
});
+
+ it('uses the passed in date parms when useCalculatedDateRange = false param is passed', async () => {
+ await timeSeriesQuery({
+ ...params,
+ useCalculatedDateRange: false,
+ query: {
+ ...params.query,
+ dateStart: '2023-10-12T00:00:00Z',
+ dateEnd: '2023-10-12T00:00:00Z',
+ },
+ });
+ // @ts-ignore
+ expect(esClient.search.mock.calls[0]![0].body.query.bool.filter[0]).toEqual({
+ range: {
+ 'time-field': {
+ format: 'strict_date_time',
+ gte: '2023-10-12T00:00:00Z',
+ lt: '2023-10-12T00:00:00Z',
+ },
+ },
+ });
+ });
});
describe('getResultFromEs', () => {
diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts
index fa6ff14bbb3e5..74d96112e7abb 100644
--- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts
+++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts
@@ -33,12 +33,19 @@ export interface TimeSeriesQueryParameters {
esClient: ElasticsearchClient;
query: TimeSeriesQuery;
condition?: TimeSeriesCondition;
+ useCalculatedDateRange?: boolean;
}
export async function timeSeriesQuery(
params: TimeSeriesQueryParameters
): Promise {
- const { logger, esClient, query: queryParams, condition: conditionParams } = params;
+ const {
+ logger,
+ esClient,
+ query: queryParams,
+ condition: conditionParams,
+ useCalculatedDateRange = true,
+ } = params;
const {
index,
timeWindowSize,
@@ -67,8 +74,8 @@ export async function timeSeriesQuery(
{
range: {
[timeField]: {
- gte: dateRangeInfo.dateStart,
- lt: dateRangeInfo.dateEnd,
+ gte: useCalculatedDateRange ? dateRangeInfo.dateStart : dateStart,
+ lt: useCalculatedDateRange ? dateRangeInfo.dateEnd : dateEnd,
format: 'strict_date_time',
},
},
diff --git a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts
index 17ce4985e1f0c..149f67cffeb32 100644
--- a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts
+++ b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts
@@ -5,12 +5,15 @@
* 2.0.
*/
-import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
+import {
+ DEFAULT_FLAPPING_SETTINGS,
+ DEFAULT_QUERY_DELAY_SETTINGS,
+} from '@kbn/alerting-plugin/common';
import { Superuser } from '../../security_and_spaces/scenarios';
import { getUrlPrefix } from './space_test_utils';
-export const resetRulesSettings = (supertest: any, space: string) => {
- return supertest
+export const resetRulesSettings = async (supertest: any, space: string) => {
+ await supertest
.post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
@@ -20,4 +23,12 @@ export const resetRulesSettings = (supertest: any, space: string) => {
status_change_threshold: DEFAULT_FLAPPING_SETTINGS.statusChangeThreshold,
})
.expect(200);
+ return supertest
+ .post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_query_delay`)
+ .set('kbn-xsrf', 'foo')
+ .auth(Superuser.username, Superuser.password)
+ .send({
+ delay: DEFAULT_QUERY_DELAY_SETTINGS.delay,
+ })
+ .expect(200);
};
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_query_delay_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_query_delay_settings.ts
new file mode 100644
index 0000000000000..ce56b6e6690bc
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_query_delay_settings.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { DEFAULT_QUERY_DELAY_SETTINGS } from '@kbn/alerting-plugin/common';
+import { UserAtSpaceScenarios } from '../../../scenarios';
+import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib';
+import { FtrProviderContext } from '../../../../common/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function getQueryDelaySettingsTests({ getService }: FtrProviderContext) {
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+
+ describe('getQueryDelaySettings', () => {
+ beforeEach(async () => {
+ await resetRulesSettings(supertestWithoutAuth, 'space1');
+ await resetRulesSettings(supertestWithoutAuth, 'space2');
+ });
+
+ after(async () => {
+ await resetRulesSettings(supertestWithoutAuth, 'space1');
+ await resetRulesSettings(supertestWithoutAuth, 'space2');
+ });
+
+ for (const scenario of UserAtSpaceScenarios) {
+ const { user, space } = scenario;
+ describe(scenario.id, () => {
+ it('should handle get query delay settings request appropriately', async () => {
+ const response = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_query_delay`)
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'space_1_all_with_restricted_fixture at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: 'Forbidden',
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.delay).to.eql(DEFAULT_QUERY_DELAY_SETTINGS.delay);
+ expect(response.body.updated_by).to.be.a('string');
+ expect(Date.parse(response.body.created_at)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+ });
+ }
+ });
+}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts
index f6247d662e527..f9af26be6def7 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/index.ts
@@ -31,6 +31,8 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
loadTestFile(require.resolve('./suggestions_value_rule'));
loadTestFile(require.resolve('./update_flapping_settings'));
loadTestFile(require.resolve('./user_managed_api_key'));
+ loadTestFile(require.resolve('./get_query_delay_settings'));
+ loadTestFile(require.resolve('./update_query_delay_settings'));
});
});
}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_query_delay_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_query_delay_settings.ts
new file mode 100644
index 0000000000000..7f7fcaa844691
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_query_delay_settings.ts
@@ -0,0 +1,103 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { DEFAULT_QUERY_DELAY_SETTINGS } from '@kbn/alerting-plugin/common';
+import { UserAtSpaceScenarios, Superuser } from '../../../scenarios';
+import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib';
+import { FtrProviderContext } from '../../../../common/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function updateQueryDelaySettingsTest({ getService }: FtrProviderContext) {
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+
+ describe('updateQueryDelaySettings', () => {
+ afterEach(async () => {
+ await resetRulesSettings(supertestWithoutAuth, 'space1');
+ await resetRulesSettings(supertestWithoutAuth, 'space2');
+ });
+
+ for (const scenario of UserAtSpaceScenarios) {
+ const { user, space } = scenario;
+ describe(scenario.id, () => {
+ it('should handle update query delay settings request appropriately', async () => {
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_query_delay`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send({
+ delay: 20,
+ });
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all at space2':
+ case 'space_1_all_with_restricted_fixture at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: 'Forbidden',
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.delay).to.eql(20);
+ expect(response.body.updated_by).to.eql(user.username);
+ expect(Date.parse(response.body.created_at)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+ });
+ }
+
+ it('should error if provided with invalid inputs', async () => {
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix('space1')}/internal/alerting/rules/settings/_query_delay`)
+ .set('kbn-xsrf', 'foo')
+ .auth(Superuser.username, Superuser.password)
+ .send({
+ delay: 200,
+ })
+ .expect(400);
+
+ expect(response.body.message).to.eql(
+ 'Invalid query delay value, must be between 0 and 60, but got: 200.'
+ );
+ });
+
+ describe('updateQueryDelaySettings for other spaces', () => {
+ it('should update specific isolated settings depending on space', async () => {
+ // Update the rules setting in space1
+ const postResponse = await supertestWithoutAuth
+ .post(`${getUrlPrefix('space1')}/internal/alerting/rules/settings/_query_delay`)
+ .set('kbn-xsrf', 'foo')
+ .auth(Superuser.username, Superuser.password)
+ .send({
+ delay: 20,
+ });
+
+ expect(postResponse.statusCode).to.eql(200);
+ expect(postResponse.body.delay).to.eql(20);
+
+ // Get the rules settings in space2
+ const getResponse = await supertestWithoutAuth
+ .get(`${getUrlPrefix('space2')}/internal/alerting/rules/settings/_query_delay`)
+ .auth(Superuser.username, Superuser.password);
+
+ expect(getResponse.statusCode).to.eql(200);
+ expect(getResponse.body.delay).to.eql(DEFAULT_QUERY_DELAY_SETTINGS.delay);
+ });
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts
index 81cceb6561bd6..e6bc3b76f74ad 100644
--- a/x-pack/test/api_integration/apis/security/privileges.ts
+++ b/x-pack/test/api_integration/apis/security/privileges.ts
@@ -105,6 +105,8 @@ export default function ({ getService }: FtrProviderContext) {
'minimal_read',
'allFlappingSettings',
'readFlappingSettings',
+ 'allQueryDelaySettings',
+ 'readQueryDelaySettings',
],
maintenanceWindow: ['all', 'read', 'minimal_all', 'minimal_read'],
guidedOnboardingFeature: ['all', 'read', 'minimal_all', 'minimal_read'],
diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts
index 174ac2a3c8f66..ecc37fdaf08ab 100644
--- a/x-pack/test/api_integration/apis/security/privileges_basic.ts
+++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts
@@ -187,6 +187,8 @@ export default function ({ getService }: FtrProviderContext) {
'minimal_read',
'allFlappingSettings',
'readFlappingSettings',
+ 'allQueryDelaySettings',
+ 'readQueryDelaySettings',
],
maintenanceWindow: ['all', 'read', 'minimal_all', 'minimal_read'],
guidedOnboardingFeature: ['all', 'read', 'minimal_all', 'minimal_read'],
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts
index 56b6d08253ec4..8c86e8a5e5965 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts
@@ -51,6 +51,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
status_change_threshold: 10,
})
.expect(200);
+ await supertest
+ .post(`/internal/alerting/rules/settings/_query_delay`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ delay: 10,
+ })
+ .expect(200);
});
beforeEach(async () => {
@@ -78,20 +85,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.waitForDeleted('centerJustifiedSpinner');
// Flapping enabled by default
- await testSubjects.missingOrFail('rulesSettingsModalFlappingOffPrompt');
+ await testSubjects.missingOrFail('rulesSettingsFlappingOffPrompt');
- await testSubjects.existOrFail('rulesSettingsModalEnableSwitch');
+ await testSubjects.existOrFail('rulesSettingsFlappingEnableSwitch');
await testSubjects.existOrFail('lookBackWindowRangeInput');
await testSubjects.existOrFail('statusChangeThresholdRangeInput');
+ await testSubjects.existOrFail('queryDelayRangeInput');
const lookBackWindowInput = await testSubjects.find('lookBackWindowRangeInput');
const statusChangeThresholdInput = await testSubjects.find('statusChangeThresholdRangeInput');
+ const queryDelayInput = await testSubjects.find('queryDelayRangeInput');
const lookBackWindowValue = await lookBackWindowInput.getAttribute('value');
const statusChangeThresholdValue = await statusChangeThresholdInput.getAttribute('value');
+ const queryDelayValue = await queryDelayInput.getAttribute('value');
expect(lookBackWindowValue).to.eql('10');
expect(statusChangeThresholdValue).to.eql('10');
+ expect(queryDelayValue).to.eql('10');
});
it('should allow the user to modify rules settings', async () => {
@@ -100,18 +111,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await dragRangeInput('lookBackWindowRangeInput', 5, 'right');
await dragRangeInput('statusChangeThresholdRangeInput', 5, 'left');
+ await dragRangeInput('queryDelayRangeInput', 5, 'left');
let lookBackWindowInput = await testSubjects.find('lookBackWindowRangeInput');
let statusChangeThresholdInput = await testSubjects.find('statusChangeThresholdRangeInput');
+ let queryDelayInput = await testSubjects.find('queryDelayRangeInput');
let lookBackWindowValue = await lookBackWindowInput.getAttribute('value');
let statusChangeThresholdValue = await statusChangeThresholdInput.getAttribute('value');
+ let queryDelayValue = await queryDelayInput.getAttribute('value');
expect(lookBackWindowValue).to.eql('15');
expect(statusChangeThresholdValue).to.eql('5');
+ expect(queryDelayValue).to.eql('5');
- await testSubjects.click('rulesSettingsModalEnableSwitch');
- await testSubjects.existOrFail('rulesSettingsModalFlappingOffPrompt');
+ await testSubjects.click('rulesSettingsFlappingEnableSwitch');
+ await testSubjects.existOrFail('rulesSettingsFlappingOffPrompt');
// Save
await testSubjects.click('rulesSettingsModalSaveButton');
@@ -123,17 +138,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.waitForDeleted('centerJustifiedSpinner');
// Flapping initially disabled
- await testSubjects.existOrFail('rulesSettingsModalFlappingOffPrompt');
- await testSubjects.click('rulesSettingsModalEnableSwitch');
+ await testSubjects.existOrFail('rulesSettingsFlappingOffPrompt');
+ await testSubjects.click('rulesSettingsFlappingEnableSwitch');
lookBackWindowInput = await testSubjects.find('lookBackWindowRangeInput');
statusChangeThresholdInput = await testSubjects.find('statusChangeThresholdRangeInput');
+ queryDelayInput = await testSubjects.find('queryDelayRangeInput');
lookBackWindowValue = await lookBackWindowInput.getAttribute('value');
statusChangeThresholdValue = await statusChangeThresholdInput.getAttribute('value');
+ queryDelayValue = await queryDelayInput.getAttribute('value');
expect(lookBackWindowValue).to.eql('15');
expect(statusChangeThresholdValue).to.eql('5');
+ expect(queryDelayValue).to.eql('5');
});
});
};
From d38921c7fca150346d58e0f19639514cec005144 Mon Sep 17 00:00:00 2001
From: Zacqary Adam Xeper
Date: Wed, 18 Oct 2023 13:29:04 -0500
Subject: [PATCH 07/14] [RAM] Disable untrack bulk action item for SIEM
(#169282)
## Summary
FIX -> https://github.com/elastic/kibana/issues/169245
Removes the Untrack bulk action from the bulk actions list if the SIEM
feature id is passed
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../sections/alerts_table/alerts_table.tsx | 1 +
.../alerts_table/alerts_table_state.tsx | 2 +
.../hooks/use_bulk_actions.test.tsx | 37 ++++++++++++++++++-
.../alerts_table/hooks/use_bulk_actions.ts | 10 ++++-
.../triggers_actions_ui/public/types.ts | 3 +-
5 files changed, 49 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx
index ddbeef1a80577..03aa3f72b73d7 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx
@@ -109,6 +109,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab
query: props.query,
useBulkActionsConfig: props.alertsTableConfiguration.useBulkActions,
refresh: alertsRefresh,
+ featureIds: props.featureIds,
});
const refreshData = useCallback(() => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx
index e7df082d95f13..94d71caac5d11 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx
@@ -407,6 +407,7 @@ const AlertsTableStateWithQueryProvider = ({
showInspectButton,
toolbarVisibility,
shouldHighlightRow,
+ featureIds,
}),
[
alertsTableConfiguration,
@@ -434,6 +435,7 @@ const AlertsTableStateWithQueryProvider = ({
showInspectButton,
toolbarVisibility,
shouldHighlightRow,
+ featureIds,
]
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx
index b6d9616b9fc7d..742a0fd3a4e87 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx
@@ -300,7 +300,7 @@ describe('bulk action hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
- it('should not how the bulk actions when the user lacks any observability permissions', () => {
+ it('should not show the bulk actions when the user lacks any observability permissions', () => {
mockKibana.mockImplementation(() => ({
services: {
application: { capabilities: {} },
@@ -371,5 +371,40 @@ describe('bulk action hooks', () => {
]
`);
});
+
+ it('appends only the case bulk actions for SIEM', async () => {
+ const { result } = renderHook(
+ () => useBulkActions({ alerts: [], query: {}, casesConfig, refresh, featureIds: ['siem'] }),
+ {
+ wrapper: appMockRender.AppWrapper,
+ }
+ );
+
+ expect(result.current.bulkActions).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "id": 0,
+ "items": Array [
+ Object {
+ "data-test-subj": "attach-new-case",
+ "disableOnQuery": true,
+ "disabledLabel": "Add to new case",
+ "key": "attach-new-case",
+ "label": "Add to new case",
+ "onClick": [Function],
+ },
+ Object {
+ "data-test-subj": "attach-existing-case",
+ "disableOnQuery": true,
+ "disabledLabel": "Add to existing case",
+ "key": "attach-existing-case",
+ "label": "Add to existing case",
+ "onClick": [Function],
+ },
+ ],
+ },
+ ]
+ `);
+ });
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts
index 47a892ef76331..90a1962d0d1f6 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts
@@ -7,7 +7,7 @@
import { useCallback, useContext, useEffect, useMemo } from 'react';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { ALERT_CASE_IDS } from '@kbn/rule-data-utils';
+import { ALERT_CASE_IDS, ValidFeatureId } from '@kbn/rule-data-utils';
import {
Alerts,
AlertsTableConfigurationRegistry,
@@ -39,6 +39,7 @@ interface BulkActionsProps {
casesConfig?: AlertsTableConfigurationRegistry['cases'];
useBulkActionsConfig?: UseBulkActionsRegistry;
refresh: () => void;
+ featureIds?: ValidFeatureId[];
}
export interface UseBulkActions {
@@ -236,6 +237,7 @@ export function useBulkActions({
query,
refresh,
useBulkActionsConfig = () => [],
+ featureIds,
}: BulkActionsProps): UseBulkActions {
const [bulkActionsState, updateBulkActionsState] = useContext(BulkActionsContext);
const configBulkActionPanels = useBulkActionsConfig(query);
@@ -253,7 +255,11 @@ export function useBulkActions({
clearSelection,
});
- const initialItems = [...caseBulkActions, ...untrackBulkActions];
+ const initialItems = [
+ ...caseBulkActions,
+ // SECURITY SOLUTION WORKAROUND: Disable untrack action for SIEM
+ ...(featureIds?.includes('siem') ? [] : untrackBulkActions),
+ ];
const bulkActions = initialItems.length
? addItemsToInitialPanel({
diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts
index 8e53f2f9aaa39..ff125d69fdfa1 100644
--- a/x-pack/plugins/triggers_actions_ui/public/types.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/types.ts
@@ -25,7 +25,7 @@ import type {
EuiSuperSelectOption,
EuiDataGridOnColumnResizeHandler,
} from '@elastic/eui';
-import type { AlertConsumers, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils';
+import type { AlertConsumers, STACK_ALERTS_FEATURE_ID, ValidFeatureId } from '@kbn/rule-data-utils';
import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui';
import { HttpSetup } from '@kbn/core/public';
import { KueryNode } from '@kbn/es-query';
@@ -560,6 +560,7 @@ export type AlertsTableProps = {
* Allows to consumers of the table to decide to highlight a row based on the current alert.
*/
shouldHighlightRow?: (alert: Alert) => boolean;
+ featureIds?: ValidFeatureId[];
} & Partial>;
// TODO We need to create generic type between our plugin, right now we have different one because of the old alerts table
From 2a5c5db78371b658a623e458dcc1a54f7fdb59de Mon Sep 17 00:00:00 2001
From: christineweng <18648970+christineweng@users.noreply.github.com>
Date: Wed, 18 Oct 2023 13:33:19 -0500
Subject: [PATCH 08/14] [Security Solution] Expandable flyout - fix deleted
rule not showing highlighted fields (#169273)
## Summary
This PR addresses https://github.com/elastic/kibana/issues/169201 and
removes the `items` override in highlighted fields table when error is
returned. Highlighted fields table should show items if they are
available.
After a rule is deleted
![image](https://github.com/elastic/kibana/assets/18648970/c3d4c51a-e211-466c-be72-a312ac52ba6a)
also indicated in rule preview
![image](https://github.com/elastic/kibana/assets/18648970/3c9ca1a6-0efa-4c58-a409-ccde542f02ed)
---
.../document_details/right/components/highlighted_fields.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx
index 41f1afbceaa7e..717cf9856651e 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx
@@ -95,7 +95,7 @@ const columns: Array> = [
export const HighlightedFields: FC = () => {
const { dataFormattedForFieldBrowser, scopeId } = useRightPanelContext();
const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
- const { loading, error, rule: maybeRule } = useRuleWithFallback(ruleId);
+ const { loading, rule: maybeRule } = useRuleWithFallback(ruleId);
const highlightedFields = useHighlightedFields({
dataFormattedForFieldBrowser,
@@ -121,7 +121,7 @@ export const HighlightedFields: FC = () => {
Date: Wed, 18 Oct 2023 20:34:33 +0200
Subject: [PATCH 09/14] [Fleet] Enforce 10 min cooldown for agent upgrade
(#168606)
## Summary
Closes https://github.com/elastic/kibana/issues/168233
This PR adds a check based on the `agent.upgraded_at` field and the time
a request to upgrade the issue. If the request is issued sooner than 10
minutes after the last upgrade, it is rejected, even if `force: true` is
passed:
- `POST agents/{agentId}/upgrade` will fail with 400
- agents included in `POST agents/bulk_upgrade` will not be upgraded
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kyle Pollich
---
x-pack/plugins/fleet/common/services/index.ts | 2 +-
.../services/is_agent_upgradeable.test.ts | 47 +++-
.../common/services/is_agent_upgradeable.ts | 24 ++
.../components/agent_upgrade_modal/index.tsx | 36 ++-
.../server/routes/agent/upgrade_handler.ts | 26 ++-
.../server/services/agents/upgrade.test.ts | 1 +
.../services/agents/upgrade_action_runner.ts | 7 +-
.../apis/agents/upgrade.ts | 212 ++++++++++++++++++
8 files changed, 335 insertions(+), 20 deletions(-)
diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts
index 04f74404ba382..663cd27deab73 100644
--- a/x-pack/plugins/fleet/common/services/index.ts
+++ b/x-pack/plugins/fleet/common/services/index.ts
@@ -18,7 +18,7 @@ export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limite
export { isValidNamespace, INVALID_NAMESPACE_CHARACTERS } from './is_valid_namespace';
export { isDiffPathProtocol } from './is_diff_path_protocol';
export { LicenseService } from './license';
-export { isAgentUpgradeable } from './is_agent_upgradeable';
+export * from './is_agent_upgradeable';
export {
isAgentRequestDiagnosticsSupported,
MINIMUM_DIAGNOSTICS_AGENT_VERSION,
diff --git a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts
index 8a3f3ce8d59ac..ad8138abbce7f 100644
--- a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts
+++ b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts
@@ -7,7 +7,7 @@
import type { Agent } from '../types/models/agent';
-import { isAgentUpgradeable } from './is_agent_upgradeable';
+import { getRecentUpgradeInfoForAgent, isAgentUpgradeable } from './is_agent_upgradeable';
const getAgent = ({
version,
@@ -15,14 +15,14 @@ const getAgent = ({
unenrolling = false,
unenrolled = false,
updating = false,
- upgraded = false,
+ minutesSinceUpgrade,
}: {
version: string;
upgradeable?: boolean;
unenrolling?: boolean;
unenrolled?: boolean;
updating?: boolean;
- upgraded?: boolean;
+ minutesSinceUpgrade?: number;
}): Agent => {
const agent: Agent = {
id: 'de9006e1-54a7-4320-b24e-927e6fe518a8',
@@ -101,8 +101,8 @@ const getAgent = ({
if (updating) {
agent.upgrade_started_at = new Date(Date.now()).toISOString();
}
- if (upgraded) {
- agent.upgraded_at = new Date(Date.now()).toISOString();
+ if (minutesSinceUpgrade) {
+ agent.upgraded_at = new Date(Date.now() - minutesSinceUpgrade * 6e4).toISOString();
}
return agent;
};
@@ -176,9 +176,42 @@ describe('Fleet - isAgentUpgradeable', () => {
isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true, updating: true }), '8.0.0')
).toBe(false);
});
- it('returns true if agent was recently upgraded', () => {
+ it('returns false if the agent reports upgradeable but was upgraded less than 10 minutes ago', () => {
expect(
- isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true, upgraded: true }), '8.0.0')
+ isAgentUpgradeable(
+ getAgent({ version: '7.9.0', upgradeable: true, minutesSinceUpgrade: 9 }),
+ '8.0.0'
+ )
+ ).toBe(false);
+ });
+ it('returns true if agent reports upgradeable and was upgraded more than 10 minutes ago', () => {
+ expect(
+ isAgentUpgradeable(
+ getAgent({ version: '7.9.0', upgradeable: true, minutesSinceUpgrade: 11 }),
+ '8.0.0'
+ )
+ ).toBe(true);
+ });
+});
+
+describe('hasAgentBeenUpgradedRecently', () => {
+ it('returns true if the agent was upgraded less than 10 minutes ago', () => {
+ expect(
+ getRecentUpgradeInfoForAgent(getAgent({ version: '7.9.0', minutesSinceUpgrade: 9 }))
+ .hasBeenUpgradedRecently
).toBe(true);
});
+
+ it('returns false if the agent was upgraded more than 10 minutes ago', () => {
+ expect(
+ getRecentUpgradeInfoForAgent(getAgent({ version: '7.9.0', minutesSinceUpgrade: 11 }))
+ .hasBeenUpgradedRecently
+ ).toBe(false);
+ });
+
+ it('returns false if the agent does not have an upgrade_at field', () => {
+ expect(
+ getRecentUpgradeInfoForAgent(getAgent({ version: '7.9.0' })).hasBeenUpgradedRecently
+ ).toBe(false);
+ });
});
diff --git a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts
index f896d6cf97bd4..c7bd21c45af4a 100644
--- a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts
+++ b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts
@@ -11,6 +11,8 @@ import semverGt from 'semver/functions/gt';
import type { Agent } from '../types';
+export const AGENT_UPGRADE_COOLDOWN_IN_MIN = 10;
+
export function isAgentUpgradeable(
agent: Agent,
latestAgentVersion: string,
@@ -32,6 +34,10 @@ export function isAgentUpgradeable(
if (agent.upgrade_started_at && !agent.upgraded_at) {
return false;
}
+ // check that the agent has not been upgraded more recently than the monitoring period
+ if (getRecentUpgradeInfoForAgent(agent).hasBeenUpgradedRecently) {
+ return false;
+ }
if (versionToUpgrade !== undefined) {
return isNotDowngrade(agentVersion, versionToUpgrade);
}
@@ -56,3 +62,21 @@ const isNotDowngrade = (agentVersion: string, versionToUpgrade: string) => {
return semverGt(versionToUpgradeNumber, agentVersionNumber);
};
+
+export function getRecentUpgradeInfoForAgent(agent: Agent): {
+ hasBeenUpgradedRecently: boolean;
+ timeToWaitMs: number;
+} {
+ if (!agent.upgraded_at) {
+ return {
+ hasBeenUpgradedRecently: false,
+ timeToWaitMs: 0,
+ };
+ }
+
+ const elaspedSinceUpgradeInMillis = Date.now() - Date.parse(agent.upgraded_at);
+ const timeToWaitMs = AGENT_UPGRADE_COOLDOWN_IN_MIN * 6e4 - elaspedSinceUpgradeInMillis;
+ const hasBeenUpgradedRecently = elaspedSinceUpgradeInMillis / 6e4 < AGENT_UPGRADE_COOLDOWN_IN_MIN;
+
+ return { hasBeenUpgradedRecently, timeToWaitMs };
+}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
index 7b35927657959..d361349b3f327 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
@@ -27,6 +27,8 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui';
import semverGt from 'semver/functions/gt';
import semverLt from 'semver/functions/lt';
+import { AGENT_UPGRADE_COOLDOWN_IN_MIN } from '../../../../../../../common/services';
+
import { getMinVersion } from '../../../../../../../common/services/get_min_max_version';
import {
AGENT_UPDATING_TIMEOUT_HOURS,
@@ -361,14 +363,32 @@ export const AgentUpgradeAgentModal: React.FunctionComponent
) : isSingleAgent ? (
-
+ <>
+
+
+
+ {isUpdating && (
+
+
+
+
+
+ )}
+ >
) : (
{
const docs = (calledWith as estypes.BulkRequest)?.body
?.filter((i: any) => i.doc)
.map((i: any) => i.doc);
+
expect(ids).toEqual(idsToAction);
for (const doc of docs!) {
expect(doc).toHaveProperty('upgrade_started_at');
diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts
index 014a9bec89739..b6ab67e5fb5e3 100644
--- a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts
+++ b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts
@@ -10,7 +10,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/
import { v4 as uuidv4 } from 'uuid';
import moment from 'moment';
-import { isAgentUpgradeable } from '../../../common/services';
+import { getRecentUpgradeInfoForAgent, isAgentUpgradeable } from '../../../common/services';
import type { Agent } from '../../types';
@@ -76,9 +76,10 @@ export async function upgradeBatch(
const latestAgentVersion = await getLatestAvailableVersion();
const upgradeableResults = await Promise.allSettled(
agentsToCheckUpgradeable.map(async (agent) => {
- // Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check
+ // Filter out agents currently unenrolling, unenrolled, recently upgraded or not upgradeable b/c of version check
const isNotAllowed =
- !options.force && !isAgentUpgradeable(agent, latestAgentVersion, options.version);
+ getRecentUpgradeInfoForAgent(agent).hasBeenUpgradedRecently ||
+ (!options.force && !isAgentUpgradeable(agent, latestAgentVersion, options.version));
if (isNotAllowed) {
throw new FleetError(`Agent ${agent.id} is not upgradeable`);
}
diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts
index b0cdbd9ece49e..0a3dc09692b68 100644
--- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts
+++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts
@@ -147,6 +147,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(400);
});
+
it('should respond 200 if upgrading agent with version the same as snapshot version and force flag is passed', async () => {
const fleetServerVersionSnapshot = makeSnapshotVersion(fleetServerVersion);
await es.update({
@@ -170,6 +171,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(200);
});
+
it('should respond 200 if upgrading agent with version less than kibana snapshot version', async () => {
const fleetServerVersionSnapshot = makeSnapshotVersion(fleetServerVersion);
@@ -191,6 +193,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(200);
});
+
it('should respond 200 if trying to upgrade with source_uri set', async () => {
await es.update({
id: 'agent1',
@@ -219,6 +222,7 @@ export default function (providerContext: FtrProviderContext) {
const action: any = actionsRes.hits.hits[0]._source;
expect(action.data.sourceURI).contain('http://path/to/download');
});
+
it('should respond 400 if trying to upgrade to a version that does not match installed kibana version', async () => {
const kibanaVersion = await kibanaServer.version.get();
const higherVersion = semver.inc(kibanaVersion, 'patch');
@@ -230,6 +234,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(400);
});
+
it('should respond 400 if trying to downgrade version', async () => {
await es.update({
id: 'agent1',
@@ -249,6 +254,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(400);
});
+
it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => {
await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({
revoke: true,
@@ -261,6 +267,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(400);
});
+
it('should respond 400 if trying to upgrade an agent that is unenrolled', async () => {
await es.update({
id: 'agent1',
@@ -344,6 +351,98 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(403);
});
+
+ it('should respond 429 if trying to upgrade a recently upgraded agent', async () => {
+ await es.update({
+ id: 'agent1',
+ refresh: 'wait_for',
+ index: AGENTS_INDEX,
+ body: {
+ doc: {
+ upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(),
+ local_metadata: {
+ elastic: {
+ agent: {
+ upgradeable: true,
+ version: '0.0.0',
+ },
+ },
+ },
+ },
+ },
+ });
+ const response = await supertest
+ .post(`/api/fleet/agents/agent1/upgrade`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ version: fleetServerVersion,
+ })
+ .expect(429);
+
+ expect(response.body.message).to.contain('was upgraded less than 10 minutes ago');
+
+ // We don't know how long this test will take to run, so we can't really assert on the actual elapsed time here
+ expect(response.body.message).to.match(/please wait \d{2}m\d{2}s/i);
+
+ expect(response.header['retry-after']).to.match(/^\d+$/);
+ });
+
+ it('should respond 429 if trying to upgrade a recently upgraded agent with force flag', async () => {
+ await es.update({
+ id: 'agent1',
+ refresh: 'wait_for',
+ index: AGENTS_INDEX,
+ body: {
+ doc: {
+ upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(),
+ local_metadata: {
+ elastic: {
+ agent: {
+ upgradeable: true,
+ version: '0.0.0',
+ },
+ },
+ },
+ },
+ },
+ });
+ await supertest
+ .post(`/api/fleet/agents/agent1/upgrade`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ version: fleetServerVersion,
+ force: true,
+ })
+ .expect(429);
+ });
+
+ it('should respond 200 if trying to upgrade an agent that was upgraded more than 10 minutes ago', async () => {
+ await es.update({
+ id: 'agent1',
+ refresh: 'wait_for',
+ index: AGENTS_INDEX,
+ body: {
+ doc: {
+ local_metadata: {
+ elastic: {
+ agent: {
+ upgradeable: true,
+ upgraded_at: new Date(Date.now() - 11 * 6e4).toString(),
+ version: '0.0.0',
+ },
+ },
+ },
+ },
+ },
+ });
+ await supertest
+ .post(`/api/fleet/agents/agent1/upgrade`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ version: fleetServerVersion,
+ })
+ .expect(200);
+ });
});
describe('multiple agents', () => {
@@ -397,6 +496,7 @@ export default function (providerContext: FtrProviderContext) {
},
});
});
+
it('should respond 200 to bulk upgrade upgradeable agents and update the agent SOs', async () => {
await es.update({
id: 'agent1',
@@ -483,6 +583,7 @@ export default function (providerContext: FtrProviderContext) {
expect(action.agents).contain('agent1');
expect(action.agents).contain('agent2');
});
+
it('should create a .fleet-actions document with the agents, version, and start_time if start_time passed', async () => {
await es.update({
id: 'agent1',
@@ -675,6 +776,7 @@ export default function (providerContext: FtrProviderContext) {
expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined');
expect(typeof agent2data.body.item.upgrade_started_at).to.be('string');
});
+
it('should not upgrade an unenrolled agent during bulk_upgrade', async () => {
await es.update({
id: 'agent1',
@@ -713,6 +815,7 @@ export default function (providerContext: FtrProviderContext) {
expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined');
expect(typeof agent2data.body.item.upgrade_started_at).to.be('string');
});
+
it('should not upgrade a non-upgradeable agent during bulk_upgrade', async () => {
const kibanaVersion = await kibanaServer.version.get();
await es.update({
@@ -765,6 +868,112 @@ export default function (providerContext: FtrProviderContext) {
expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined');
expect(typeof agent3data.body.item.upgrade_started_at).to.be('undefined');
});
+
+ it('should not upgrade a recently upgraded agent during bulk_upgrade', async () => {
+ await es.update({
+ id: 'agent1',
+ refresh: 'wait_for',
+ index: AGENTS_INDEX,
+ body: {
+ doc: {
+ upgraded_at: new Date(Date.now() - 11 * 6e4).toISOString(),
+ local_metadata: {
+ elastic: {
+ agent: {
+ upgradeable: true,
+ version: '0.0.0',
+ },
+ },
+ },
+ },
+ },
+ });
+ await es.update({
+ id: 'agent2',
+ refresh: 'wait_for',
+ index: AGENTS_INDEX,
+ body: {
+ doc: {
+ upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(),
+ local_metadata: {
+ elastic: {
+ agent: {
+ upgradeable: true,
+ version: '0.0.0',
+ },
+ },
+ },
+ },
+ },
+ });
+ await supertest
+ .post(`/api/fleet/agents/bulk_upgrade`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ agents: ['agent1', 'agent2'],
+ version: fleetServerVersion,
+ });
+ const [agent1data, agent2data] = await Promise.all([
+ supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'),
+ supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'),
+ ]);
+ expect(typeof agent1data.body.item.upgrade_started_at).to.be('string');
+ expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined');
+ });
+
+ it('should not upgrade a recently upgraded agent during bulk_upgrade even with force flag', async () => {
+ await es.update({
+ id: 'agent1',
+ refresh: 'wait_for',
+ index: AGENTS_INDEX,
+ body: {
+ doc: {
+ upgraded_at: new Date(Date.now() - 11 * 6e4).toISOString(),
+ local_metadata: {
+ elastic: {
+ agent: {
+ upgradeable: true,
+ version: '0.0.0',
+ },
+ },
+ },
+ },
+ },
+ });
+ await es.update({
+ id: 'agent2',
+ refresh: 'wait_for',
+ index: AGENTS_INDEX,
+ body: {
+ doc: {
+ upgraded_at: new Date(Date.now() - 9 * 6e4).toISOString(),
+ local_metadata: {
+ elastic: {
+ agent: {
+ upgradeable: true,
+ version: '0.0.0',
+ },
+ },
+ },
+ },
+ },
+ });
+ await supertest
+ .post(`/api/fleet/agents/bulk_upgrade`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ agents: ['agent1', 'agent2'],
+ version: fleetServerVersion,
+ force: true,
+ });
+ const [agent1data, agent2data] = await Promise.all([
+ supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'),
+ supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'),
+ ]);
+ expect(typeof agent1data.body.item.upgrade_started_at).to.be('string');
+ expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined');
+ });
+
it('should upgrade a non upgradeable agent during bulk_upgrade with force flag', async () => {
await es.update({
id: 'agent1',
@@ -817,6 +1026,7 @@ export default function (providerContext: FtrProviderContext) {
expect(typeof agent2data.body.item.upgrade_started_at).to.be('string');
expect(typeof agent3data.body.item.upgrade_started_at).to.be('string');
});
+
it('should respond 400 if trying to bulk upgrade to a version that is higher than the latest installed kibana version', async () => {
const kibanaVersion = await kibanaServer.version.get();
const higherVersion = semver.inc(kibanaVersion, 'patch');
@@ -851,6 +1061,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(400);
});
+
it('should respond 400 if trying to bulk upgrade to a version that is higher than the latest fleet server version', async () => {
const higherVersion = semver.inc(fleetServerVersion, 'patch');
await es.update({
@@ -884,6 +1095,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(400);
});
+
it('should prevent any agent to downgrade', async () => {
await es.update({
id: 'agent1',
From 1416ff5a136ab6f9f36aee194deac5f927f69551 Mon Sep 17 00:00:00 2001
From: Shahzad
Date: Wed, 18 Oct 2023 21:01:07 +0200
Subject: [PATCH 10/14] [Synthetics] Use agent policy namespace for monitors if
custom not defined (#169225)
---
.../synthetics_private_locations.ts | 1 +
.../common/types/synthetics_monitor.ts | 1 +
.../private_locations/get_agent_policies.ts | 19 ++++++++++++
.../settings/private_locations/helpers.ts | 25 +++++++++------
.../server/runtime_types/private_locations.ts | 1 +
.../synthetics_private_location.ts | 31 ++++++++++++++-----
.../add_monitor_private_location.ts | 1 +
.../apis/synthetics/sync_global_params.ts | 1 +
8 files changed, 63 insertions(+), 17 deletions(-)
diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts
index c0366d8e3935f..d1ee7898ccbd3 100644
--- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts
+++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_private_locations.ts
@@ -22,6 +22,7 @@ export const PrivateLocationCodec = t.intersection([
lat: t.number,
lon: t.number,
}),
+ namespace: t.string,
}),
]);
diff --git a/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts b/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts
index 47b0eaf9d143c..a4697d1c38776 100644
--- a/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts
+++ b/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts
@@ -27,4 +27,5 @@ export interface AgentPolicyInfo {
agents: number;
status: string;
description?: string;
+ namespace?: string;
}
diff --git a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts
index 668beba0a8f95..53858b0516dc6 100644
--- a/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts
+++ b/x-pack/plugins/synthetics/server/routes/settings/private_locations/get_agent_policies.ts
@@ -39,5 +39,24 @@ export const getAgentPoliciesAsInternalUser = async (server: SyntheticsServerSet
agents: agentPolicy.agents ?? 0,
status: agentPolicy.status,
description: agentPolicy.description,
+ namespace: agentPolicy.namespace,
}));
};
+
+export const getAgentPolicyAsInternalUser = async (server: SyntheticsServerSetup, id: string) => {
+ const soClient = server.coreStart.savedObjects.createInternalRepository();
+
+ const agentPolicy = await server.fleet?.agentPolicyService.get(soClient, id);
+ if (!agentPolicy) {
+ return null;
+ }
+
+ return {
+ id: agentPolicy.id,
+ name: agentPolicy.name,
+ agents: agentPolicy.agents ?? 0,
+ status: agentPolicy.status,
+ description: agentPolicy.description,
+ namespace: agentPolicy.namespace,
+ };
+};
diff --git a/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts
index b41c8f5e7538d..9cccb0fc9a543 100644
--- a/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts
+++ b/x-pack/plugins/synthetics/server/routes/settings/private_locations/helpers.ts
@@ -17,16 +17,20 @@ export const toClientContract = (
agentPolicies?: AgentPolicyInfo[]
): SyntheticsPrivateLocations => {
return {
- locations: attributes.locations.map((location) => ({
- label: location.label,
- id: location.id,
- agentPolicyId: location.agentPolicyId,
- concurrentMonitors: location.concurrentMonitors,
- isServiceManaged: false,
- isInvalid: !Boolean(agentPolicies?.find((policy) => policy.id === location.agentPolicyId)),
- tags: location.tags,
- geo: location.geo,
- })),
+ locations: attributes.locations.map((location) => {
+ const agPolicy = agentPolicies?.find((policy) => policy.id === location.agentPolicyId);
+ return {
+ label: location.label,
+ id: location.id,
+ agentPolicyId: location.agentPolicyId,
+ concurrentMonitors: location.concurrentMonitors,
+ isServiceManaged: false,
+ isInvalid: !Boolean(agPolicy),
+ tags: location.tags,
+ geo: location.geo,
+ namespace: agPolicy?.namespace,
+ };
+ }),
};
};
@@ -39,5 +43,6 @@ export const toSavedObjectContract = (location: PrivateLocation): PrivateLocatio
tags: location.tags,
isServiceManaged: false,
geo: location.geo,
+ namespace: location.namespace,
};
};
diff --git a/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts b/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts
index d8b4e41ede17a..d7841cb2aca63 100644
--- a/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts
+++ b/x-pack/plugins/synthetics/server/runtime_types/private_locations.ts
@@ -21,6 +21,7 @@ export const PrivateLocationAttributesCodec = t.intersection([
lat: t.number,
lon: t.number,
}),
+ namespace: t.string,
}),
]);
diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts
index d4e1a78977e36..7aa1b2570c68c 100644
--- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts
+++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts
@@ -8,12 +8,16 @@ import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { NewPackagePolicyWithId } from '@kbn/fleet-plugin/server/services/package_policy';
import { cloneDeep } from 'lodash';
import { SavedObjectError } from '@kbn/core-saved-objects-common';
+import { DEFAULT_NAMESPACE_STRING } from '../../../common/constants/monitor_defaults';
import {
BROWSER_TEST_NOW_RUN,
LIGHTWEIGHT_TEST_NOW_RUN,
} from '../synthetics_monitor/synthetics_monitor_client';
import { scheduleCleanUpTask } from './clean_up_task';
-import { getAgentPoliciesAsInternalUser } from '../../routes/settings/private_locations/get_agent_policies';
+import {
+ getAgentPoliciesAsInternalUser,
+ getAgentPolicyAsInternalUser,
+} from '../../routes/settings/private_locations/get_agent_policies';
import { SyntheticsServerSetup } from '../../types';
import { formatSyntheticsPolicy } from '../formatters/private_formatters/format_synthetics_policy';
import {
@@ -66,7 +70,7 @@ export class SyntheticsPrivateLocation {
return `${config.id}-${locId}-${spaceId}`;
}
- generateNewPolicy(
+ async generateNewPolicy(
config: HeartbeatConfig,
privateLocation: PrivateLocationAttributes,
newPolicyTemplate: NewPackagePolicy,
@@ -74,7 +78,7 @@ export class SyntheticsPrivateLocation {
globalParams: Record,
testRunId?: string,
runOnce?: boolean
- ): (NewPackagePolicy & { policy_id: string }) | null {
+ ): Promise<(NewPackagePolicy & { policy_id: string }) | null> {
const { label: locName } = privateLocation;
const newPolicy = cloneDeep(newPolicyTemplate);
@@ -92,7 +96,9 @@ export class SyntheticsPrivateLocation {
newPolicy.name = `${config[ConfigKey.NAME]}-${locName}-${spaceId}`;
}
}
- newPolicy.namespace = config[ConfigKey.NAMESPACE];
+ const configNameSpace = config[ConfigKey.NAMESPACE];
+
+ newPolicy.namespace = await this.getPolicyNameSpace(configNameSpace, privateLocation);
const { formattedPolicy } = formatSyntheticsPolicy(
newPolicy,
@@ -152,7 +158,7 @@ export class SyntheticsPrivateLocation {
);
}
- const newPolicy = this.generateNewPolicy(
+ const newPolicy = await this.generateNewPolicy(
config,
location,
newPolicyTemplate,
@@ -226,7 +232,7 @@ export class SyntheticsPrivateLocation {
const location = allPrivateLocations?.find((loc) => loc.id === privateLocation?.id)!;
- const newPolicy = this.generateNewPolicy(
+ const newPolicy = await this.generateNewPolicy(
config,
location,
newPolicyTemplate,
@@ -278,7 +284,7 @@ export class SyntheticsPrivateLocation {
const hasPolicy = existingPolicies?.some((policy) => policy.id === currId);
try {
if (hasLocation) {
- const newPolicy = this.generateNewPolicy(
+ const newPolicy = await this.generateNewPolicy(
config,
privateLocation,
newPolicyTemplate,
@@ -437,6 +443,17 @@ export class SyntheticsPrivateLocation {
async getAgentPolicies() {
return await getAgentPoliciesAsInternalUser(this.server);
}
+
+ async getPolicyNameSpace(configNameSpace: string, privateLocation: PrivateLocationAttributes) {
+ if (configNameSpace && configNameSpace !== DEFAULT_NAMESPACE_STRING) {
+ return configNameSpace;
+ }
+ if (privateLocation.namespace) {
+ return privateLocation.namespace;
+ }
+ const agentPolicy = await getAgentPolicyAsInternalUser(this.server, privateLocation.id);
+ return agentPolicy?.namespace ?? DEFAULT_NAMESPACE_STRING;
+ }
}
const throwAddEditError = (hasPolicy: boolean, location?: string, name?: string) => {
diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts
index 79a2267cbadef..83359d4e75b3e 100644
--- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts
+++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts
@@ -81,6 +81,7 @@ export default function ({ getService }: FtrProviderContext) {
lon: 0,
},
agentPolicyId: testFleetPolicyID,
+ namespace: 'default',
},
]);
});
diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts
index 765138034f772..7424634955951 100644
--- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts
+++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts
@@ -89,6 +89,7 @@ export default function ({ getService }: FtrProviderContext) {
lon: '',
},
agentPolicyId: testFleetPolicyID,
+ namespace: 'default',
},
]);
});
From 8359bad7ecb5d936ac5feaac54e5b812d8f0b1ec Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Wed, 18 Oct 2023 20:15:05 +0100
Subject: [PATCH 11/14] fix(NA): typecheck error
---
.../alerting/metric_threshold/metric_threshold_executor.test.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index f3bd6972eeea6..65fa6e5a070ce 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -42,6 +42,8 @@ const logger = {
const mockNow = new Date('2023-09-20T15:11:04.105Z');
+const STARTED_AT_MOCK_DATE = new Date();
+
const mockOptions = {
executionId: '',
startedAt: mockNow,
From 8bd62dda2cb6278202be1a3c9f1f23142d9630f8 Mon Sep 17 00:00:00 2001
From: Jon
Date: Wed, 18 Oct 2023 14:51:23 -0500
Subject: [PATCH 12/14] =?UTF-8?q?Revert=20"[ci]=20Temporarily=20move=20osq?=
=?UTF-8?q?uery=20tests=20back=20to=20on=5Fmerge=5Funsuppor=E2=80=A6=20(#1?=
=?UTF-8?q?69278)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
…ted pipeline (#169189)"
This reverts commit 267fdb1ace81fc68a4435f2906da3f52f6252808.
---
.buildkite/pipelines/on_merge.yml | 12 +++++++
.../pipelines/on_merge_unsupported_ftrs.yml | 12 -------
.buildkite/pipelines/pull_request/base.yml | 34 +++++++++++++++++++
.../pull_request/osquery_cypress.yml | 34 -------------------
.../pipelines/pull_request/pipeline.ts | 8 -----
5 files changed, 46 insertions(+), 54 deletions(-)
delete mode 100644 .buildkite/pipelines/pull_request/osquery_cypress.yml
diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml
index 5c587545897f5..815e4d9adb5e2 100644
--- a/.buildkite/pipelines/on_merge.yml
+++ b/.buildkite/pipelines/on_merge.yml
@@ -187,6 +187,18 @@ steps:
- exit_status: '*'
limit: 1
+ - command: .buildkite/scripts/steps/functional/osquery_cypress.sh
+ label: 'Osquery Cypress Tests'
+ agents:
+ queue: n2-4-spot
+ depends_on: build
+ timeout_in_minutes: 50
+ parallelism: 6
+ retry:
+ automatic:
+ - exit_status: '*'
+ limit: 1
+
- command: '.buildkite/scripts/steps/functional/on_merge_unsupported_ftrs.sh'
label: Trigger unsupported ftr tests
timeout_in_minutes: 10
diff --git a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml
index 904bed2b042ab..6dee27db71659 100644
--- a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml
+++ b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml
@@ -63,15 +63,3 @@ steps:
limit: 3
- exit_status: '*'
limit: 1
-
- - command: .buildkite/scripts/steps/functional/osquery_cypress.sh
- label: 'Osquery Cypress Tests'
- agents:
- queue: n2-4-spot
- depends_on: build
- timeout_in_minutes: 50
- parallelism: 6
- retry:
- automatic:
- - exit_status: '*'
- limit: 1
diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml
index 5213dfc0e4ab1..c1cd68c6b04ab 100644
--- a/.buildkite/pipelines/pull_request/base.yml
+++ b/.buildkite/pipelines/pull_request/base.yml
@@ -187,6 +187,18 @@ steps:
- exit_status: '*'
limit: 1
+ - command: .buildkite/scripts/steps/functional/osquery_cypress.sh
+ label: 'Osquery Cypress Tests'
+ agents:
+ queue: n2-4-spot
+ depends_on: build
+ timeout_in_minutes: 50
+ parallelism: 6
+ retry:
+ automatic:
+ - exit_status: '*'
+ limit: 1
+
- command: .buildkite/scripts/steps/functional/security_solution_burn.sh
label: 'Security Solution Cypress tests, burning changed specs'
agents:
@@ -198,6 +210,28 @@ steps:
automatic: false
soft_fail: true
+ - command: .buildkite/scripts/steps/functional/osquery_cypress_burn.sh
+ label: 'Osquery Cypress Tests, burning changed specs'
+ agents:
+ queue: n2-4-spot
+ depends_on: build
+ timeout_in_minutes: 50
+ soft_fail: true
+ retry:
+ automatic: false
+
+ - command: .buildkite/scripts/steps/functional/security_serverless_osquery.sh
+ label: 'Serverless Osquery Cypress Tests'
+ agents:
+ queue: n2-4-spot
+ depends_on: build
+ timeout_in_minutes: 50
+ parallelism: 6
+ retry:
+ automatic:
+ - exit_status: '*'
+ limit: 1
+
# status_exception: Native role management is not enabled in this Elasticsearch instance
# - command: .buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh
# label: 'Serverless Security Defend Workflows Cypress Tests'
diff --git a/.buildkite/pipelines/pull_request/osquery_cypress.yml b/.buildkite/pipelines/pull_request/osquery_cypress.yml
deleted file mode 100644
index 49ef00aeb8090..0000000000000
--- a/.buildkite/pipelines/pull_request/osquery_cypress.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-steps:
- - command: .buildkite/scripts/steps/functional/osquery_cypress.sh
- label: 'Osquery Cypress Tests'
- agents:
- queue: n2-4-spot
- depends_on: build
- timeout_in_minutes: 50
- parallelism: 6
- retry:
- automatic:
- - exit_status: '*'
- limit: 1
-
- - command: .buildkite/scripts/steps/functional/security_serverless_osquery.sh
- label: 'Serverless Osquery Cypress Tests'
- agents:
- queue: n2-4-spot
- depends_on: build
- timeout_in_minutes: 50
- parallelism: 6
- retry:
- automatic:
- - exit_status: '*'
- limit: 1
-
- - command: .buildkite/scripts/steps/functional/osquery_cypress_burn.sh
- label: 'Osquery Cypress Tests, burning changed specs'
- agents:
- queue: n2-4-spot
- depends_on: build
- timeout_in_minutes: 50
- soft_fail: true
- retry:
- automatic: false
diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts
index 4d6cd774393e0..7a7fa0f59b9c7 100644
--- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts
+++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts
@@ -151,14 +151,6 @@ const uploadPipeline = (pipelineContent: string | object) => {
pipeline.push(getPipeline('.buildkite/pipelines/pull_request/webpack_bundle_analyzer.yml'));
}
- if (
- ((await doAnyChangesMatch([/^x-pack\/plugins\/osquery/, /^x-pack\/test\/osquery_cypress/])) ||
- GITHUB_PR_LABELS.includes('ci:all-cypress-suites')) &&
- !GITHUB_PR_LABELS.includes('ci:skip-cypress-osquery')
- ) {
- pipeline.push(getPipeline('.buildkite/pipelines/pull_request/osquery_cypress.yml'));
- }
-
if (
(await doAnyChangesMatch([
/\.docnav\.json$/,
From 6fd7a867ea7ba56db8beb3210b7893288f191915 Mon Sep 17 00:00:00 2001
From: Ash <1849116+ashokaditya@users.noreply.github.com>
Date: Wed, 18 Oct 2023 22:47:14 +0200
Subject: [PATCH 13/14] [Security Solution] [Endpoint] Upload action endpoint
tests and other missing workflow tests for response console (#168005)
## Summary
Adds missing tests for upload response action workflows.
Adds tests for
- [x] upload response action
- [x] responder action from Alerts -> Alert details
- [x] responder action from Cases -> Alert details
- [x] responder action from Timeline view -> Alert Details
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Patryk Kopycinski
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../fleet/server/services/files/mocks.ts | 4 +-
.../history_log.cy.ts | 6 +-
.../alerts_response_console.cy.ts | 115 +++++++++++++++++
.../cases_response_console.cy.ts | 122 ++++++++++++++++++
.../endpoints_list_response_console.cy.ts | 69 ++++++++++
.../e2e/response_actions/isolate.cy.ts | 10 +-
.../isolate_mocked_data.cy.ts | 8 +-
...e.cy.ts => response_console_actions.cy.ts} | 20 ++-
.../management/cypress/screens/alerts.ts | 21 ++-
.../management/cypress/screens/responder.ts | 2 +-
.../public/management/cypress/support/e2e.ts | 12 +-
.../management/cypress/tasks/isolate.ts | 8 +-
12 files changed, 372 insertions(+), 25 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts
create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/cases_response_console.cy.ts
create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/endpoints_list_response_console.cy.ts
rename x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/{response_console.cy.ts => response_console_actions.cy.ts} (94%)
diff --git a/x-pack/plugins/fleet/server/services/files/mocks.ts b/x-pack/plugins/fleet/server/services/files/mocks.ts
index 23c0482b7e111..2000f8eefc02b 100644
--- a/x-pack/plugins/fleet/server/services/files/mocks.ts
+++ b/x-pack/plugins/fleet/server/services/files/mocks.ts
@@ -10,12 +10,12 @@ import { Readable } from 'stream';
import type { estypes } from '@elastic/elasticsearch';
import type {
+ FleetFile,
FleetFromHostFileClientInterface,
FleetToHostFileClientInterface,
HapiReadableStream,
+ HostUploadedFileMetadata,
} from './types';
-import type { FleetFile } from './types';
-import type { HostUploadedFileMetadata } from './types';
export const createFleetFromHostFilesClientMock =
(): jest.Mocked => {
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts
index b2f99a51087be..dbe75b576ac9c 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts
@@ -22,8 +22,6 @@ describe(
const [endpointAgentId, endpointHostname] = generateRandomStringName(2);
before(() => {
- login(ROLE.endpoint_response_actions_access);
-
indexEndpointHosts({ numResponseActions: 2 }).then((indexEndpoints) => {
endpointData = indexEndpoints;
});
@@ -59,6 +57,10 @@ describe(
}
});
+ beforeEach(() => {
+ login(ROLE.endpoint_response_actions_access);
+ });
+
it('enable filtering by type', () => {
cy.visit(`/app/security/administration/response_actions_history`);
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts
new file mode 100644
index 0000000000000..a736e05c33145
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts
@@ -0,0 +1,115 @@
+/*
+ * 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 { closeAllToasts } from '../../tasks/toasts';
+import {
+ getAlertsTableRows,
+ openAlertDetailsView,
+ openInvestigateInTimelineView,
+ openResponderFromEndpointAlertDetails,
+} from '../../screens/alerts';
+import { ensureOnResponder } from '../../screens/responder';
+import { cleanupRule, loadRule } from '../../tasks/api_fixtures';
+import type { PolicyData } from '../../../../../common/endpoint/types';
+import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
+import { waitForEndpointListPageToBeLoaded } from '../../tasks/response_console';
+import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
+import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
+import { toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate';
+
+import { login } from '../../tasks/login';
+import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
+import { createEndpointHost } from '../../tasks/create_endpoint_host';
+import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
+
+describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
+ let indexedPolicy: IndexedFleetEndpointPolicyResponse;
+ let policy: PolicyData;
+ let createdHost: CreateAndEnrollEndpointHostResponse;
+
+ beforeEach(() => {
+ login();
+ });
+
+ before(() => {
+ getEndpointIntegrationVersion().then((version) =>
+ createAgentPolicyTask(version).then((data) => {
+ indexedPolicy = data;
+ policy = indexedPolicy.integrationPolicies[0];
+
+ return enableAllPolicyProtections(policy.id).then(() => {
+ // Create and enroll a new Endpoint host
+ return createEndpointHost(policy.policy_id).then((host) => {
+ createdHost = host as CreateAndEnrollEndpointHostResponse;
+ });
+ });
+ })
+ );
+ });
+
+ after(() => {
+ if (createdHost) {
+ cy.task('destroyEndpointHost', createdHost);
+ }
+
+ if (indexedPolicy) {
+ cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
+ }
+
+ if (createdHost) {
+ deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
+ }
+ });
+
+ describe('From Alerts', () => {
+ let ruleId: string;
+ let ruleName: string;
+
+ before(() => {
+ loadRule(
+ { query: `agent.name: ${createdHost.hostname} and agent.type: endpoint` },
+ false
+ ).then((data) => {
+ ruleId = data.id;
+ ruleName = data.name;
+ });
+ });
+
+ after(() => {
+ if (ruleId) {
+ cleanupRule(ruleId);
+ }
+ });
+
+ it('should open responder from alert details flyout', () => {
+ waitForEndpointListPageToBeLoaded(createdHost.hostname);
+ toggleRuleOffAndOn(ruleName);
+ visitRuleAlerts(ruleName);
+ closeAllToasts();
+ getAlertsTableRows().should('have.length.greaterThan', 0);
+ openAlertDetailsView();
+
+ openResponderFromEndpointAlertDetails();
+ ensureOnResponder();
+ });
+
+ it('should open responder from timeline view alert details flyout', () => {
+ waitForEndpointListPageToBeLoaded(createdHost.hostname);
+ toggleRuleOffAndOn(ruleName);
+ visitRuleAlerts(ruleName);
+ closeAllToasts();
+
+ getAlertsTableRows().should('have.length.greaterThan', 0);
+ openInvestigateInTimelineView();
+ cy.getByTestSubj('timeline-flyout').within(() => {
+ openAlertDetailsView();
+ });
+ openResponderFromEndpointAlertDetails();
+ ensureOnResponder();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/cases_response_console.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/cases_response_console.cy.ts
new file mode 100644
index 0000000000000..a33d325d5443a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/cases_response_console.cy.ts
@@ -0,0 +1,122 @@
+/*
+ * 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 { loadPage } from '../../tasks/common';
+import { closeAllToasts } from '../../tasks/toasts';
+import {
+ addAlertToCase,
+ getAlertsTableRows,
+ openAlertDetailsView,
+ openResponderFromEndpointAlertDetails,
+} from '../../screens/alerts';
+import { ensureOnResponder } from '../../screens/responder';
+import { cleanupCase, cleanupRule, loadCase, loadRule } from '../../tasks/api_fixtures';
+import type { PolicyData } from '../../../../../common/endpoint/types';
+import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
+import { waitForEndpointListPageToBeLoaded } from '../../tasks/response_console';
+import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
+import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
+import { openCaseAlertDetails, toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate';
+
+import { login } from '../../tasks/login';
+import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
+import { createEndpointHost } from '../../tasks/create_endpoint_host';
+import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
+import { APP_CASES_PATH } from '../../../../../common/constants';
+
+describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
+ let indexedPolicy: IndexedFleetEndpointPolicyResponse;
+ let policy: PolicyData;
+ let createdHost: CreateAndEnrollEndpointHostResponse;
+
+ before(() => {
+ getEndpointIntegrationVersion().then((version) =>
+ createAgentPolicyTask(version).then((data) => {
+ indexedPolicy = data;
+ policy = indexedPolicy.integrationPolicies[0];
+
+ return enableAllPolicyProtections(policy.id).then(() => {
+ // Create and enroll a new Endpoint host
+ return createEndpointHost(policy.policy_id).then((host) => {
+ createdHost = host as CreateAndEnrollEndpointHostResponse;
+ });
+ });
+ })
+ );
+ });
+
+ beforeEach(() => {
+ login();
+ });
+
+ after(() => {
+ if (createdHost) {
+ cy.task('destroyEndpointHost', createdHost);
+ }
+
+ if (indexedPolicy) {
+ cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
+ }
+
+ if (createdHost) {
+ deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
+ }
+ });
+
+ describe('From Cases', () => {
+ let ruleId: string;
+ let ruleName: string;
+ let caseId: string;
+ const caseOwner = 'securitySolution';
+
+ beforeEach(() => {
+ loadRule(
+ { query: `agent.name: ${createdHost.hostname} and agent.type: endpoint` },
+ false
+ ).then((data) => {
+ ruleId = data.id;
+ ruleName = data.name;
+ });
+ loadCase(caseOwner).then((data) => {
+ caseId = data.id;
+ });
+ });
+
+ afterEach(() => {
+ if (ruleId) {
+ cleanupRule(ruleId);
+ }
+ if (caseId) {
+ cleanupCase(caseId);
+ }
+ });
+
+ it('should open responder', () => {
+ waitForEndpointListPageToBeLoaded(createdHost.hostname);
+ toggleRuleOffAndOn(ruleName);
+ visitRuleAlerts(ruleName);
+ closeAllToasts();
+
+ getAlertsTableRows().should('have.length.greaterThan', 0);
+ openAlertDetailsView();
+ addAlertToCase(caseId, caseOwner);
+
+ // visit case details page
+ cy.intercept('GET', `/api/cases/${caseId}/user_actions/_find*`).as('case');
+ loadPage(`${APP_CASES_PATH}/${caseId}`);
+
+ cy.wait('@case', { timeout: 30000 }).then(({ response: res }) => {
+ const caseAlertId = res?.body.userActions[1].id;
+ closeAllToasts();
+ openCaseAlertDetails(caseAlertId);
+ });
+
+ openResponderFromEndpointAlertDetails();
+ ensureOnResponder();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/endpoints_list_response_console.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/endpoints_list_response_console.cy.ts
new file mode 100644
index 0000000000000..75074b0d3f94a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/endpoints_list_response_console.cy.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { ensureOnResponder } from '../../screens/responder';
+import type { PolicyData } from '../../../../../common/endpoint/types';
+import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
+import {
+ openResponseConsoleFromEndpointList,
+ waitForEndpointListPageToBeLoaded,
+} from '../../tasks/response_console';
+import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
+import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
+
+import { login } from '../../tasks/login';
+import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
+import { createEndpointHost } from '../../tasks/create_endpoint_host';
+import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
+
+describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
+ beforeEach(() => {
+ login();
+ });
+
+ describe('From endpoint list', () => {
+ let indexedPolicy: IndexedFleetEndpointPolicyResponse;
+ let policy: PolicyData;
+ let createdHost: CreateAndEnrollEndpointHostResponse;
+
+ before(() => {
+ getEndpointIntegrationVersion().then((version) =>
+ createAgentPolicyTask(version).then((data) => {
+ indexedPolicy = data;
+ policy = indexedPolicy.integrationPolicies[0];
+
+ return enableAllPolicyProtections(policy.id).then(() => {
+ // Create and enroll a new Endpoint host
+ return createEndpointHost(policy.policy_id).then((host) => {
+ createdHost = host as CreateAndEnrollEndpointHostResponse;
+ });
+ });
+ })
+ );
+ });
+
+ after(() => {
+ if (createdHost) {
+ cy.task('destroyEndpointHost', createdHost);
+ }
+
+ if (indexedPolicy) {
+ cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
+ }
+
+ if (createdHost) {
+ deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
+ }
+ });
+
+ it('should open responder', () => {
+ waitForEndpointListPageToBeLoaded(createdHost.hostname);
+ openResponseConsoleFromEndpointList();
+ ensureOnResponder();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate.cy.ts
index f09e0b462a75d..ada452213b74d 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate.cy.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { openAlertDetailsView } from '../../screens/alerts';
import type { PolicyData } from '../../../../../common/endpoint/types';
import { APP_CASES_PATH, APP_ENDPOINTS_PATH } from '../../../../../common/constants';
import { closeAllToasts } from '../../tasks/toasts';
@@ -14,7 +15,6 @@ import {
checkFlyoutEndpointIsolation,
filterOutIsolatedHosts,
isolateHostWithComment,
- openAlertDetails,
openCaseAlertDetails,
releaseHostWithComment,
toggleRuleOffAndOn,
@@ -139,7 +139,7 @@ describe.skip('Isolate command', { tags: ['@ess', '@serverless', '@brokenInServe
visitRuleAlerts(ruleName);
closeAllToasts();
- openAlertDetails();
+ openAlertDetailsView();
isolateHostWithComment(isolateComment, createdHost.hostname);
@@ -147,7 +147,7 @@ describe.skip('Isolate command', { tags: ['@ess', '@serverless', '@brokenInServe
cy.contains(`Isolation on host ${createdHost.hostname} successfully submitted`);
cy.getByTestSubj('euiFlyoutCloseButton').click();
- openAlertDetails();
+ openAlertDetailsView();
checkFlyoutEndpointIsolation();
@@ -156,7 +156,7 @@ describe.skip('Isolate command', { tags: ['@ess', '@serverless', '@brokenInServe
cy.contains(`Release on host ${createdHost.hostname} successfully submitted`);
cy.getByTestSubj('euiFlyoutCloseButton').click();
- openAlertDetails();
+ openAlertDetailsView();
cy.getByTestSubj('event-field-agent.status').within(() => {
cy.get('[title="Isolated"]').should('not.exist');
});
@@ -205,7 +205,7 @@ describe.skip('Isolate command', { tags: ['@ess', '@serverless', '@brokenInServe
visitRuleAlerts(ruleName);
closeAllToasts();
- openAlertDetails();
+ openAlertDetailsView();
cy.getByTestSubj('add-to-existing-case-action').click();
cy.getByTestSubj(`cases-table-row-select-${caseId}`).click();
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate_mocked_data.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate_mocked_data.cy.ts
index 00bca7e3864af..a630d8fc4ec01 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate_mocked_data.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/isolate_mocked_data.cy.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { openAlertDetailsView } from '../../screens/alerts';
import { getEndpointListPath } from '../../../common/routing';
import {
checkEndpointIsIsolated,
@@ -12,7 +13,6 @@ import {
filterOutIsolatedHosts,
interceptActionRequests,
isolateHostWithComment,
- openAlertDetails,
openCaseAlertDetails,
releaseHostWithComment,
sendActionResponse,
@@ -148,7 +148,7 @@ describe('Isolate command', { tags: ['@ess', '@serverless'] }, () => {
});
});
- openAlertDetails();
+ openAlertDetailsView();
isolateHostWithComment(isolateComment, hostname);
@@ -167,7 +167,7 @@ describe('Isolate command', { tags: ['@ess', '@serverless'] }, () => {
cy.getByTestSubj('euiFlyoutCloseButton').click();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
- openAlertDetails();
+ openAlertDetailsView();
checkFlyoutEndpointIsolation();
@@ -185,7 +185,7 @@ describe('Isolate command', { tags: ['@ess', '@serverless'] }, () => {
cy.contains(`Release on host ${hostname} successfully submitted`);
cy.getByTestSubj('euiFlyoutCloseButton').click();
- openAlertDetails();
+ openAlertDetailsView();
cy.getByTestSubj('event-field-agent.status').within(() => {
cy.get('[title="Isolated"]').should('not.exist');
});
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console_actions.cy.ts
similarity index 94%
rename from x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console.cy.ts
rename to x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console_actions.cy.ts
index 9690107c2d218..b727697da17be 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console_actions.cy.ts
@@ -16,7 +16,7 @@ import {
waitForEndpointListPageToBeLoaded,
} from '../../tasks/response_console';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
-import { getEndpointIntegrationVersion, createAgentPolicyTask } from '../../tasks/fleet';
+import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
import {
checkEndpointListForOnlyIsolatedHosts,
checkEndpointListForOnlyUnIsolatedHosts,
@@ -188,7 +188,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles
});
});
- describe('File operations: get-file and execute', () => {
+ describe('File operations: get-file, upload and execute', () => {
const homeFilePath = process.env.CI || true ? '/home/vagrant' : `/home/ubuntu`;
const fileContent = 'This is a test file for the get-file command.';
@@ -271,6 +271,22 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles
submitCommand();
waitForCommandToBeExecuted('execute');
});
+
+ it('"upload --file" - should upload a file', () => {
+ waitForEndpointListPageToBeLoaded(createdHost.hostname);
+ openResponseConsoleFromEndpointList();
+ inputConsoleCommand(`upload --file`);
+ cy.getByTestSubj('console-arg-file-picker').selectFile(
+ {
+ contents: Cypress.Buffer.from('upload file content here!'),
+ fileName: 'upload_file.txt',
+ lastModified: Date.now(),
+ },
+ { force: true }
+ );
+ submitCommand();
+ waitForCommandToBeExecuted('upload');
+ });
});
// FLAKY: https://github.com/elastic/kibana/issues/168296
diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts
index a1e53cee9b09a..b434458c4dc1d 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts
@@ -13,7 +13,7 @@ export const navigateToAlertsList = (urlQueryParams: string = '') => {
};
export const clickAlertListRefreshButton = (): Cypress.Chainable => {
- cy.getByTestSubj('querySubmitButton').click();
+ cy.getByTestSubj('querySubmitButton').first().click();
return cy.getByTestSubj('querySubmitButton').should('be.enabled');
};
@@ -41,3 +41,22 @@ export const getAlertsTableRows = (timeout?: number): Cypress.Chainable $rows);
};
+
+export const openAlertDetailsView = (rowIndex: number = 0): void => {
+ cy.getByTestSubj('expand-event').eq(rowIndex).click();
+ cy.getByTestSubj('take-action-dropdown-btn').click();
+};
+
+export const openInvestigateInTimelineView = (): void => {
+ cy.getByTestSubj('send-alert-to-timeline-button').first().click();
+};
+
+export const openResponderFromEndpointAlertDetails = (): void => {
+ cy.getByTestSubj('endpointResponseActions-action-item').click();
+};
+
+export const addAlertToCase = (caseId: string, caseOwner: string): void => {
+ cy.getByTestSubj('add-to-existing-case-action').click();
+ cy.getByTestSubj(`cases-table-row-select-${caseId}`).click();
+ cy.contains(`An alert was added to \"Test ${caseOwner} case`);
+};
diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts
index c9e320728ee23..c612c99db17b3 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/screens/responder.ts
@@ -30,7 +30,7 @@ export const getConsoleHelpPanelResponseActionTestSubj = (): Record<
};
};
-const ensureOnResponder = (): Cypress.Chainable> => {
+export const ensureOnResponder = (): Cypress.Chainable> => {
return cy.getByTestSubj(TEST_SUBJ.responderPage).should('exist');
};
diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts b/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts
index 7c8bf5104524b..67c2a3ef0375e 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts
@@ -23,12 +23,13 @@
// ***********************************************************
import { subj as testSubjSelector } from '@kbn/test-subj-selector';
-
import 'cypress-react-selector';
-
// @ts-ignore
import registerCypressGrep from '@cypress/grep';
+import { login, ROLE } from '../tasks/login';
+import { loadPage } from '../tasks/common';
+
registerCypressGrep();
Cypress.Commands.addQuery<'getByTestSubj'>(
@@ -100,3 +101,10 @@ Cypress.Commands.add(
);
Cypress.on('uncaught:exception', () => false);
+
+// Login as a Platform Engineer to properly initialize Security Solution App
+before(() => {
+ login(ROLE.soc_manager);
+ loadPage('/app/security/alerts');
+ cy.getByTestSubj('manage-alert-detection-rules').should('exist');
+});
diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts
index a15a71f1362eb..e13bb832adce5 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts
@@ -7,6 +7,7 @@
/* eslint-disable cypress/no-unnecessary-waiting */
+import { openAlertDetailsView } from '../screens/alerts';
import type { ActionDetails } from '../../../../common/endpoint/types';
import { loadPage } from './common';
@@ -46,11 +47,6 @@ export const releaseHostWithComment = (comment: string, hostname: string): void
cy.getByTestSubj('host_isolation_comment').type(comment);
};
-export const openAlertDetails = (): void => {
- cy.getByTestSubj('expand-event').first().click();
- cy.getByTestSubj('take-action-dropdown-btn').click();
-};
-
export const openCaseAlertDetails = (alertId: string): void => {
cy.getByTestSubj(`comment-action-show-alert-${alertId}`).click();
cy.getByTestSubj('take-action-dropdown-btn').click();
@@ -84,7 +80,7 @@ export const checkFlyoutEndpointIsolation = (): void => {
} else {
cy.getByTestSubj('euiFlyoutCloseButton').click();
cy.wait(5000);
- openAlertDetails();
+ openAlertDetailsView();
cy.getByTestSubj('event-field-agent.status').within(() => {
cy.contains('Isolated');
});
From 8fd827f8683cc0312ba957e384d82bab5ab217c7 Mon Sep 17 00:00:00 2001
From: Kurt
Date: Wed, 18 Oct 2023 16:59:15 -0400
Subject: [PATCH 14/14] Upgrade babel/traverse to 7.23.2 (#169174)
## Summary
Upgrading `@babel/traverse` form 7.21.2 to 7.23.2
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
package.json | 2 +-
yarn.lock | 90 +++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 83 insertions(+), 9 deletions(-)
diff --git a/package.json b/package.json
index 8947a3c3680b0..d2ce2ddb94eab 100644
--- a/package.json
+++ b/package.json
@@ -1093,7 +1093,7 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@babel/register": "^7.21.0",
- "@babel/traverse": "^7.21.2",
+ "@babel/traverse": "^7.23.2",
"@babel/types": "^7.21.2",
"@bazel/ibazel": "^0.16.2",
"@bazel/typescript": "4.6.2",
diff --git a/yarn.lock b/yarn.lock
index 1dcea68abfd5b..751fcd443ce48 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -196,6 +196,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
+"@babel/generator@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
+ integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
+ dependencies:
+ "@babel/types" "^7.23.0"
+ "@jridgewell/gen-mapping" "^0.3.2"
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jsesc "^2.5.1"
+
"@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
@@ -275,6 +285,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
+"@babel/helper-environment-visitor@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
+ integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
+
"@babel/helper-explode-assignable-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
@@ -290,6 +305,14 @@
"@babel/template" "^7.20.7"
"@babel/types" "^7.21.0"
+"@babel/helper-function-name@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
+ integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
+ dependencies:
+ "@babel/template" "^7.22.15"
+ "@babel/types" "^7.23.0"
+
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
@@ -297,6 +320,13 @@
dependencies:
"@babel/types" "^7.18.6"
+"@babel/helper-hoist-variables@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
+ integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5"
@@ -385,11 +415,23 @@
dependencies:
"@babel/types" "^7.18.6"
+"@babel/helper-split-export-declaration@^7.22.6":
+ version "7.22.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
+ integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
"@babel/helper-string-parser@^7.19.4":
version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
+"@babel/helper-string-parser@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
+ integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
+
"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
@@ -452,6 +494,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea"
integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==
+"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
+ integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
+
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@@ -1261,6 +1308,15 @@
"@babel/parser" "^7.20.7"
"@babel/types" "^7.20.7"
+"@babel/template@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
+ integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/parser" "^7.22.15"
+ "@babel/types" "^7.22.15"
+
"@babel/traverse@^7.10.3", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.19.0", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.4.5":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.2.tgz#ac7e1f27658750892e815e60ae90f382a46d8e75"
@@ -1277,6 +1333,22 @@
debug "^4.1.0"
globals "^11.1.0"
+"@babel/traverse@^7.23.2":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
+ integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/generator" "^7.23.0"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
+ "@babel/helper-hoist-variables" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/parser" "^7.23.0"
+ "@babel/types" "^7.23.0"
+ debug "^4.1.0"
+ globals "^11.1.0"
+
"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
version "7.21.2"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.2.tgz#92246f6e00f91755893c2876ad653db70c8310d1"
@@ -1286,6 +1358,15 @@
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
+"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
+ integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
+ dependencies:
+ "@babel/helper-string-parser" "^7.22.5"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ to-fast-properties "^2.0.0"
+
"@base2/pretty-print-object@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4"
@@ -27155,7 +27236,7 @@ semver@5.6.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
-semver@7.5.4:
+semver@7.5.4, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@@ -27167,13 +27248,6 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semve
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4:
- version "7.5.4"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
- integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
- dependencies:
- lru-cache "^6.0.0"
-
send@0.17.2:
version "0.17.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820"