diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 20bbbcf874c05..c748d63484e28 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -53,8 +53,12 @@ You can configure the following settings in the `kibana.yml` file. + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. +| `xpack.actions` +`.preconfiguredAlertHistoryEsIndex` {ess-icon} + | Enables a preconfigured alert history {es} <> connector. Defaults to `false`. + | `xpack.actions.preconfigured` - | Specifies preconfigured action IDs and configs. Defaults to {}. + | Specifies preconfigured connector IDs and configs. Defaults to {}. | `xpack.actions.proxyUrl` {ess-icon} | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc index 80226e737e9c0..53fab8bf2242f 100644 --- a/docs/user/alerting/action-types/index.asciidoc +++ b/docs/user/alerting/action-types/index.asciidoc @@ -82,3 +82,38 @@ PUT test } } -------------------------------------------------- + +[float] +[[preconfigured-connector-alert-history]] +=== Alert history {es} index connector + +experimental[] {kib} offers a preconfigured index connector to facilitate indexing active alert data into {es}. + +[WARNING] +================================================== +This functionality is experimental and may be changed or removed completely in a future release. +================================================== + +To use this connector, set the <> configuration to `true`. + +```js + xpack.actions.preconfiguredAlertHistoryEsIndex: true +``` + +When creating a new rule, add an <> and select the `Alert history Elasticsearch index (preconfigured)` connector. + +[role="screenshot"] +image::images/pre-configured-alert-history-connector.png[Select pre-configured alert history connectors] + +Documents are indexed using a preconfigured schema that captures the <> available for the rule. By default, these documents are indexed into the `kibana-alert-history-default` index, but you can specify a different index. Index names must start with `kibana-alert-history-` to take advantage of the preconfigured alert history index template. + +[IMPORTANT] +============================================== +To write documents to the preconfigured index, you must have `all` or `write` privileges to the `kibana-alert-history-*` indices. Refer to <> for more information. +============================================== + +[NOTE] +================================================== +The `kibana-alert-history-*` indices are not configured to use ILM so they must be maintained manually. If the index size grows large, +consider using the {ref}/docs-delete-by-query.html[delete by query] API to clean up older documents in the index. +================================================== \ No newline at end of file diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc index ee8a28a864824..557404f24288a 100644 --- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -51,6 +51,14 @@ two out-of-the box connectors: <> and <>. ============================================== +[float] +[[build-in-preconfigured-connectors]] +==== Built-in preconfigured connectors + +{kib} provides one built-in preconfigured connector: + +* <> + [float] [[managing-pre-configured-connectors]] ==== View preconfigured connectors @@ -63,4 +71,4 @@ image::images/pre-configured-connectors-managing.png[Connectors managing tab wit Clicking a preconfigured connector shows the description, but not the configuration. A message indicates that this is a preconfigured connector. [role="screenshot"] -image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] +image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] \ No newline at end of file diff --git a/docs/user/alerting/images/pre-configured-alert-history-connector.png b/docs/user/alerting/images/pre-configured-alert-history-connector.png new file mode 100644 index 0000000000000..35f9b19710cda Binary files /dev/null and b/docs/user/alerting/images/pre-configured-alert-history-connector.png differ diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index b6c9177b794f0..5e2c54853d648 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -210,6 +210,7 @@ export class DocLinksService { indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-index-threshold.html`, pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`, preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-connectors.html`, + preconfiguredAlertHistoryConnector: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html#preconfigured-connector-alert-history`, serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`, setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`, slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`, diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 504c3147a6f9d..db36c9cf248e7 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -159,6 +159,7 @@ kibana_vars=( xpack.actions.allowedHosts xpack.actions.enabled xpack.actions.enabledActionTypes + xpack.actions.preconfiguredAlertHistoryEsIndex xpack.actions.preconfigured xpack.actions.proxyHeaders xpack.actions.proxyRejectUnauthorizedCertificates diff --git a/x-pack/plugins/actions/common/alert_history_schema.test.ts b/x-pack/plugins/actions/common/alert_history_schema.test.ts new file mode 100644 index 0000000000000..42a3d98c85fc7 --- /dev/null +++ b/x-pack/plugins/actions/common/alert_history_schema.test.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 { buildAlertHistoryDocument } from './alert_history_schema'; + +function getVariables(overrides = {}) { + return { + date: '2021-01-01T00:00:00.000Z', + rule: { + id: 'rule-id', + name: 'rule-name', + type: 'rule-type', + spaceId: 'space-id', + }, + context: { + contextVar1: 'contextValue1', + contextVar2: 'contextValue2', + }, + params: { + ruleParam: 1, + ruleParamString: 'another param', + }, + tags: ['abc', 'def'], + alert: { + id: 'alert-id', + actionGroup: 'action-group-id', + actionGroupName: 'Action Group', + }, + ...overrides, + }; +} + +describe('buildAlertHistoryDocument', () => { + it('handles empty variables', () => { + expect(buildAlertHistoryDocument({})).toBeNull(); + }); + + it('returns null if rule type is not defined', () => { + expect(buildAlertHistoryDocument(getVariables({ rule: { type: undefined } }))).toBeNull(); + }); + + it('returns null if alert variables are not defined', () => { + expect(buildAlertHistoryDocument(getVariables({ alert: undefined }))).toBeNull(); + }); + + it('returns null if rule variables are not defined', () => { + expect(buildAlertHistoryDocument(getVariables({ rule: undefined }))).toBeNull(); + }); + + it('includes @timestamp field if date is null', () => { + const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ date: undefined })); + expect(alertHistoryDoc).not.toBeNull(); + expect(alertHistoryDoc!['@timestamp']).toBeTruthy(); + }); + + it(`doesn't include context if context is empty`, () => { + const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ context: {} })); + expect(alertHistoryDoc).not.toBeNull(); + expect(alertHistoryDoc!.kibana?.alert?.context).toBeFalsy(); + }); + + it(`doesn't include params if params is empty`, () => { + const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ params: {} })); + expect(alertHistoryDoc).not.toBeNull(); + expect(alertHistoryDoc!.rule?.params).toBeFalsy(); + }); + + it(`doesn't include tags if tags is empty array`, () => { + const alertHistoryDoc = buildAlertHistoryDocument(getVariables({ tags: [] })); + expect(alertHistoryDoc).not.toBeNull(); + expect(alertHistoryDoc!.tags).toBeFalsy(); + }); + + it(`included message if context contains message`, () => { + const alertHistoryDoc = buildAlertHistoryDocument( + getVariables({ + context: { contextVar1: 'contextValue1', contextVar2: 'contextValue2', message: 'hello!' }, + }) + ); + expect(alertHistoryDoc).not.toBeNull(); + expect(alertHistoryDoc!.message).toEqual('hello!'); + }); + + it('builds alert history document from variables', () => { + expect(buildAlertHistoryDocument(getVariables())).toEqual({ + '@timestamp': '2021-01-01T00:00:00.000Z', + kibana: { + alert: { + actionGroup: 'action-group-id', + actionGroupName: 'Action Group', + context: { + 'rule-type': { + contextVar1: 'contextValue1', + contextVar2: 'contextValue2', + }, + }, + id: 'alert-id', + }, + }, + event: { + kind: 'alert', + }, + rule: { + id: 'rule-id', + name: 'rule-name', + params: { + 'rule-type': { + ruleParam: 1, + ruleParamString: 'another param', + }, + }, + space: 'space-id', + type: 'rule-type', + }, + tags: ['abc', 'def'], + }); + }); +}); diff --git a/x-pack/plugins/actions/common/alert_history_schema.ts b/x-pack/plugins/actions/common/alert_history_schema.ts new file mode 100644 index 0000000000000..e1c923ab23f44 --- /dev/null +++ b/x-pack/plugins/actions/common/alert_history_schema.ts @@ -0,0 +1,90 @@ +/* + * 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 { isEmpty } from 'lodash'; + +export const ALERT_HISTORY_PREFIX = 'kibana-alert-history-'; +export const AlertHistoryDefaultIndexName = `${ALERT_HISTORY_PREFIX}default`; +export const AlertHistoryEsIndexConnectorId = 'preconfigured-alert-history-es-index'; + +export const buildAlertHistoryDocument = (variables: Record) => { + const { date, alert: alertVariables, context, params, tags, rule: ruleVariables } = variables as { + date: string; + alert: Record; + context: Record; + params: Record; + rule: Record; + tags: string[]; + }; + + if (!alertVariables || !ruleVariables) { + return null; + } + + const { actionGroup, actionGroupName, id: alertId } = alertVariables as { + actionGroup: string; + actionGroupName: string; + id: string; + }; + + const { id: ruleId, name, spaceId, type } = ruleVariables as { + id: string; + name: string; + spaceId: string; + type: string; + }; + + if (!type) { + // can't build the document without a type + return null; + } + + const ruleType = type.replace(/\./g, '__'); + + const rule = { + ...(ruleId ? { id: ruleId } : {}), + ...(name ? { name } : {}), + ...(!isEmpty(params) ? { params: { [ruleType]: params } } : {}), + ...(spaceId ? { space: spaceId } : {}), + ...(type ? { type } : {}), + }; + const alert = { + ...(alertId ? { id: alertId } : {}), + ...(!isEmpty(context) ? { context: { [ruleType]: context } } : {}), + ...(actionGroup ? { actionGroup } : {}), + ...(actionGroupName ? { actionGroupName } : {}), + }; + + const alertHistoryDoc = { + '@timestamp': date ? date : new Date().toISOString(), + ...(tags && tags.length > 0 ? { tags } : {}), + ...(context?.message ? { message: context.message } : {}), + ...(!isEmpty(rule) ? { rule } : {}), + ...(!isEmpty(alert) ? { kibana: { alert } } : {}), + }; + + return !isEmpty(alertHistoryDoc) ? { ...alertHistoryDoc, event: { kind: 'alert' } } : null; +}; + +export const AlertHistoryDocumentTemplate = Object.freeze( + buildAlertHistoryDocument({ + rule: { + id: '{{rule.id}}', + name: '{{rule.name}}', + type: '{{rule.type}}', + spaceId: '{{rule.spaceId}}', + }, + context: '{{context}}', + params: '{{params}}', + tags: '{{rule.tags}}', + alert: { + id: '{{alert.id}}', + actionGroup: '{{alert.actionGroup}}', + actionGroupName: '{{alert.actionGroupName}}', + }, + }) +); diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index 184ae9c226b8f..336aa2263af0c 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -6,7 +6,7 @@ */ export * from './types'; +export * from './alert_history_schema'; +export * from './rewrite_request_case'; export const BASE_ACTION_API_PATH = '/api/actions'; - -export * from './rewrite_request_case'; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 6544a3c426e42..ae7faca1465c7 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -405,6 +405,7 @@ describe('create()', () => { enabled: true, enabledActionTypes: ['some-not-ignored-action-type'], allowedHosts: ['*'], + preconfiguredAlertHistoryEsIndex: false, preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index c81f1f4a4bf2e..1b9de0162f340 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -18,6 +18,7 @@ const defaultActionsConfig: ActionsConfig = { enabled: false, allowedHosts: [], enabledActionTypes: [], + preconfiguredAlertHistoryEsIndex: false, preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index 282ff22f770f0..5c0f720e8c5fc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -18,6 +18,7 @@ import { ESIndexActionType, ESIndexActionTypeExecutorOptions, } from './es_index'; +import { AlertHistoryEsIndexConnectorId } from '../../common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../src/core/server/elasticsearch/client/mocks'; @@ -115,6 +116,7 @@ describe('params validation', () => { test('params validation succeeds when params is valid', () => { const params: Record = { documents: [{ rando: 'thing' }], + indexOverride: null, }; expect(validateParams(actionType, params)).toMatchInlineSnapshot(` Object { @@ -123,6 +125,7 @@ describe('params validation', () => { "rando": "thing", }, ], + "indexOverride": null, } `); }); @@ -159,6 +162,7 @@ describe('execute()', () => { config = { index: 'index-value', refresh: false, executionTimeField: null }; params = { documents: [{ jim: 'bob' }], + indexOverride: null, }; const actionId = 'some-id'; @@ -200,6 +204,7 @@ describe('execute()', () => { config = { index: 'index-value', executionTimeField: 'field_to_use_for_time', refresh: true }; params = { documents: [{ jimbob: 'jr' }], + indexOverride: null, }; executorOptions = { actionId, config, secrets, params, services }; @@ -237,6 +242,7 @@ describe('execute()', () => { config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ jim: 'bob' }], + indexOverride: null, }; executorOptions = { actionId, config, secrets, params, services }; @@ -270,6 +276,7 @@ describe('execute()', () => { config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ a: 1 }, { b: 2 }], + indexOverride: null, }; executorOptions = { actionId, config, secrets, params, services }; @@ -305,12 +312,244 @@ describe('execute()', () => { `); }); + test('renders parameter templates as expected', async () => { + expect(actionType.renderParameterTemplates).toBeTruthy(); + const paramsWithTemplates = { + documents: [{ hello: '{{who}}' }], + indexOverride: null, + }; + const variables = { + who: 'world', + }; + const renderedParams = actionType.renderParameterTemplates!( + paramsWithTemplates, + variables, + 'action-type-id' + ); + expect(renderedParams).toMatchInlineSnapshot(` + Object { + "documents": Array [ + Object { + "hello": "world", + }, + ], + "indexOverride": null, + } + `); + }); + + test('ignores indexOverride for generic es index connector', async () => { + expect(actionType.renderParameterTemplates).toBeTruthy(); + const paramsWithTemplates = { + documents: [{ hello: '{{who}}' }], + indexOverride: 'hello-world', + }; + const variables = { + who: 'world', + }; + const renderedParams = actionType.renderParameterTemplates!( + paramsWithTemplates, + variables, + 'action-type-id' + ); + expect(renderedParams).toMatchInlineSnapshot(` + Object { + "documents": Array [ + Object { + "hello": "world", + }, + ], + "indexOverride": null, + } + `); + }); + + test('renders parameter templates as expected for preconfigured alert history connector', async () => { + expect(actionType.renderParameterTemplates).toBeTruthy(); + const paramsWithTemplates = { + documents: [{ hello: '{{who}}' }], + indexOverride: null, + }; + const variables = { + date: '2021-01-01T00:00:00.000Z', + rule: { + id: 'rule-id', + name: 'rule-name', + type: 'rule-type', + }, + context: { + contextVar1: 'contextValue1', + contextVar2: 'contextValue2', + }, + params: { + ruleParam: 1, + ruleParamString: 'another param', + }, + tags: ['abc', 'xyz'], + alert: { + id: 'alert-id', + actionGroup: 'action-group-id', + actionGroupName: 'Action Group', + }, + state: { + alertStateValue: true, + alertStateAnotherValue: 'yes', + }, + }; + const renderedParams = actionType.renderParameterTemplates!( + paramsWithTemplates, + variables, + AlertHistoryEsIndexConnectorId + ); + expect(renderedParams).toMatchInlineSnapshot(` + Object { + "documents": Array [ + Object { + "@timestamp": "2021-01-01T00:00:00.000Z", + "event": Object { + "kind": "alert", + }, + "kibana": Object { + "alert": Object { + "actionGroup": "action-group-id", + "actionGroupName": "Action Group", + "context": Object { + "rule-type": Object { + "contextVar1": "contextValue1", + "contextVar2": "contextValue2", + }, + }, + "id": "alert-id", + }, + }, + "rule": Object { + "id": "rule-id", + "name": "rule-name", + "params": Object { + "rule-type": Object { + "ruleParam": 1, + "ruleParamString": "another param", + }, + }, + "type": "rule-type", + }, + "tags": Array [ + "abc", + "xyz", + ], + }, + ], + "indexOverride": null, + } + `); + }); + + test('passes through indexOverride for preconfigured alert history connector', async () => { + expect(actionType.renderParameterTemplates).toBeTruthy(); + const paramsWithTemplates = { + documents: [{ hello: '{{who}}' }], + indexOverride: 'hello-world', + }; + const variables = { + date: '2021-01-01T00:00:00.000Z', + rule: { + id: 'rule-id', + name: 'rule-name', + type: 'rule-type', + }, + context: { + contextVar1: 'contextValue1', + contextVar2: 'contextValue2', + }, + params: { + ruleParam: 1, + ruleParamString: 'another param', + }, + tags: ['abc', 'xyz'], + alert: { + id: 'alert-id', + actionGroup: 'action-group-id', + actionGroupName: 'Action Group', + }, + state: { + alertStateValue: true, + alertStateAnotherValue: 'yes', + }, + }; + const renderedParams = actionType.renderParameterTemplates!( + paramsWithTemplates, + variables, + AlertHistoryEsIndexConnectorId + ); + expect(renderedParams).toMatchInlineSnapshot(` + Object { + "documents": Array [ + Object { + "@timestamp": "2021-01-01T00:00:00.000Z", + "event": Object { + "kind": "alert", + }, + "kibana": Object { + "alert": Object { + "actionGroup": "action-group-id", + "actionGroupName": "Action Group", + "context": Object { + "rule-type": Object { + "contextVar1": "contextValue1", + "contextVar2": "contextValue2", + }, + }, + "id": "alert-id", + }, + }, + "rule": Object { + "id": "rule-id", + "name": "rule-name", + "params": Object { + "rule-type": Object { + "ruleParam": 1, + "ruleParamString": "another param", + }, + }, + "type": "rule-type", + }, + "tags": Array [ + "abc", + "xyz", + ], + }, + ], + "indexOverride": "hello-world", + } + `); + }); + + test('throws error for preconfigured alert history index when no variables are available', async () => { + expect(actionType.renderParameterTemplates).toBeTruthy(); + const paramsWithTemplates = { + documents: [{ hello: '{{who}}' }], + indexOverride: null, + }; + const variables = {}; + + expect(() => + actionType.renderParameterTemplates!( + paramsWithTemplates, + variables, + AlertHistoryEsIndexConnectorId + ) + ).toThrowErrorMatchingInlineSnapshot( + `"error creating alert history document for ${AlertHistoryEsIndexConnectorId} connector"` + ); + }); + test('resolves with an error when an error occurs in the indexing operation', async () => { const secrets = {}; // minimal params const config = { index: 'index-value', refresh: false, executionTimeField: null }; const params = { documents: [{ '': 'bob' }], + indexOverride: null, }; const actionId = 'some-id'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 997c89b38a340..865f12850d30c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -8,9 +8,11 @@ import { curry, find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; - import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; +import { renderMustacheObject } from '../lib/mustache_renderer'; +import { buildAlertHistoryDocument, AlertHistoryEsIndexConnectorId } from '../../common'; +import { ALERT_HISTORY_PREFIX } from '../../common/alert_history_schema'; export type ESIndexActionType = ActionType; export type ESIndexActionTypeExecutorOptions = ActionTypeExecutorOptions< @@ -38,6 +40,15 @@ export type ActionParamsType = TypeOf; // eventually: https://github.com/elastic/kibana/projects/26#card-24087404 const ParamsSchema = schema.object({ documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + indexOverride: schema.nullable( + schema.string({ + validate: (pattern) => { + if (!pattern.startsWith(ALERT_HISTORY_PREFIX)) { + return `index must start with "${ALERT_HISTORY_PREFIX}"`; + } + }, + }) + ), }); export const ActionTypeId = '.index'; @@ -54,6 +65,7 @@ export function getActionType({ logger }: { logger: Logger }): ESIndexActionType params: ParamsSchema, }, executor: curry(executor)({ logger }), + renderParameterTemplates, }; } @@ -68,7 +80,7 @@ async function executor( const params = execOptions.params; const services = execOptions.services; - const index = config.index; + const index = params.indexOverride || config.index; const bulkBody = []; for (const document of params.documents) { @@ -107,6 +119,24 @@ async function executor( } } +function renderParameterTemplates( + params: ActionParamsType, + variables: Record, + actionId: string +): ActionParamsType { + const { documents, indexOverride } = renderMustacheObject(params, variables); + + if (actionId === AlertHistoryEsIndexConnectorId) { + const alertHistoryDoc = buildAlertHistoryDocument(variables); + if (!alertHistoryDoc) { + throw new Error(`error creating alert history document for ${actionId} connector`); + } + return { documents: [alertHistoryDoc], indexOverride }; + } + + return { documents, indexOverride: null }; +} + function wrapErr( errMessage: string, actionId: string, diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 2eecaa19da0c5..ad598bffe04b4 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -31,6 +31,7 @@ describe('config validation', () => { "valueInBytes": 1048576, }, "preconfigured": Object {}, + "preconfiguredAlertHistoryEsIndex": false, "proxyRejectUnauthorizedCertificates": true, "rejectUnauthorized": true, "responseTimeout": "PT1M", @@ -74,6 +75,7 @@ describe('config validation', () => { "secrets": Object {}, }, }, + "preconfiguredAlertHistoryEsIndex": false, "proxyRejectUnauthorizedCertificates": false, "rejectUnauthorized": false, "responseTimeout": "PT1M", diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 4aa77ded315b8..36948478816c9 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -37,6 +37,7 @@ export const configSchema = schema.object({ defaultValue: [AllowedHosts.Any], } ), + preconfiguredAlertHistoryEsIndex: schema.boolean({ defaultValue: false }), preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, { defaultValue: {}, validate: validatePreconfigured, diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index ab29f524c202d..4d32c2e2bf16d 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -40,10 +40,11 @@ const createStartMock = () => { // this is a default renderer that escapes nothing export function renderActionParameterTemplatesDefault( actionTypeId: string, + actionId: string, params: Record, variables: Record ) { - return renderActionParameterTemplates(undefined, actionTypeId, params, variables); + return renderActionParameterTemplates(undefined, actionTypeId, actionId, params, variables); } const createServicesMock = () => { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 30bbedbedbe9c..3485891a01267 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -23,6 +23,7 @@ import { ActionsPluginsStart, PluginSetupContract, } from './plugin'; +import { AlertHistoryEsIndexConnectorId } from '../common'; describe('Actions Plugin', () => { describe('setup()', () => { @@ -36,6 +37,7 @@ describe('Actions Plugin', () => { enabled: true, enabledActionTypes: ['*'], allowedHosts: ['*'], + preconfiguredAlertHistoryEsIndex: false, preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, @@ -180,6 +182,7 @@ describe('Actions Plugin', () => { }); describe('start()', () => { + let context: PluginInitializerContext; let plugin: ActionsPlugin; let coreSetup: ReturnType; let coreStart: ReturnType; @@ -187,10 +190,11 @@ describe('Actions Plugin', () => { let pluginsStart: jest.Mocked; beforeEach(() => { - const context = coreMock.createPluginInitializerContext({ + context = coreMock.createPluginInitializerContext({ enabled: true, enabledActionTypes: ['*'], allowedHosts: ['*'], + preconfiguredAlertHistoryEsIndex: false, preconfigured: { preconfiguredServerLog: { actionTypeId: '.server-log', @@ -223,15 +227,6 @@ describe('Actions Plugin', () => { }); describe('getActionsClientWithRequest()', () => { - it('should handle preconfigured actions', async () => { - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = await plugin.start(coreStart, pluginsStart); - - expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); - }); - it('should not throw error when ESO plugin has encryption key', async () => { await plugin.setup(coreSetup, { ...pluginsSetup, @@ -258,6 +253,99 @@ describe('Actions Plugin', () => { }); }); + describe('Preconfigured connectors', () => { + function getConfig(overrides = {}) { + return { + enabled: true, + enabledActionTypes: ['*'], + allowedHosts: ['*'], + preconfiguredAlertHistoryEsIndex: false, + preconfigured: { + preconfiguredServerLog: { + actionTypeId: '.server-log', + name: 'preconfigured-server-log', + config: {}, + secrets: {}, + }, + }, + proxyRejectUnauthorizedCertificates: true, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, + rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration('60s'), + ...overrides, + }; + } + + function setup(config: ActionsConfig) { + context = coreMock.createPluginInitializerContext(config); + plugin = new ActionsPlugin(context); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + pluginsSetup = { + taskManager: taskManagerMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), + licensing: licensingMock.createSetup(), + eventLog: eventLogMock.createSetup(), + usageCollection: usageCollectionPluginMock.createSetupContract(), + features: featuresPluginMock.createSetup(), + }; + pluginsStart = { + licensing: licensingMock.createStart(), + taskManager: taskManagerMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + }; + } + + it('should handle preconfigured actions', async () => { + setup(getConfig()); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); + const pluginStart = await plugin.start(coreStart, pluginsStart); + + expect(pluginStart.preconfiguredActions.length).toEqual(1); + expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); + }); + + it('should handle preconfiguredAlertHistoryEsIndex = true', async () => { + setup(getConfig({ preconfiguredAlertHistoryEsIndex: true })); + + await plugin.setup(coreSetup, pluginsSetup); + const pluginStart = await plugin.start(coreStart, pluginsStart); + + expect(pluginStart.preconfiguredActions.length).toEqual(2); + expect( + pluginStart.isActionExecutable('preconfigured-alert-history-es-index', '.index') + ).toBe(true); + }); + + it('should not allow preconfigured connector with same ID as AlertHistoryEsIndexConnectorId', async () => { + setup( + getConfig({ + preconfigured: { + [AlertHistoryEsIndexConnectorId]: { + actionTypeId: '.index', + name: 'clashing preconfigured index connector', + config: {}, + secrets: {}, + }, + }, + }) + ); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); + const pluginStart = await plugin.start(coreStart, pluginsStart); + + expect(pluginStart.preconfiguredActions.length).toEqual(0); + expect(context.logger.get().warn).toHaveBeenCalledWith( + `Preconfigured connectors cannot have the id "${AlertHistoryEsIndexConnectorId}" because this is a reserved id.` + ); + }); + }); + describe('isActionTypeEnabled()', () => { const actionType: ActionType = { id: 'my-action-type', diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index bfe3b0a09ff2e..3c754d90c4af5 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -68,6 +68,9 @@ import { } from './authorization/get_authorization_mode_by_source'; import { ensureSufficientLicense } from './lib/ensure_sufficient_license'; import { renderMustacheObject } from './lib/mustache_renderer'; +import { getAlertHistoryEsIndex } from './preconfigured_connectors/alert_history_es_index/alert_history_es_index'; +import { createAlertHistoryIndexTemplate } from './preconfigured_connectors/alert_history_es_index/create_alert_history_index_template'; +import { AlertHistoryEsIndexConnectorId } from '../common'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -98,6 +101,7 @@ export interface PluginStartContract { preconfiguredActions: PreConfiguredAction[]; renderActionParameterTemplates( actionTypeId: string, + actionId: string, params: Params, variables: Record ): Params; @@ -178,12 +182,22 @@ export class ActionsPlugin implements Plugin { return this.actionTypeRegistry!.isActionTypeEnabled(id, options); @@ -468,12 +489,13 @@ export class ActionsPlugin implements Plugin( actionTypeRegistry: ActionTypeRegistry | undefined, actionTypeId: string, + actionId: string, params: Params, variables: Record ): Params { const actionType = actionTypeRegistry?.get(actionTypeId); if (actionType?.renderParameterTemplates) { - return actionType.renderParameterTemplates(params, variables) as Params; + return actionType.renderParameterTemplates(params, variables, actionId) as Params; } else { return renderMustacheObject(params, variables); } diff --git a/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/alert_history_es_index.ts b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/alert_history_es_index.ts new file mode 100644 index 0000000000000..38556591c4ea2 --- /dev/null +++ b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/alert_history_es_index.ts @@ -0,0 +1,26 @@ +/* + * 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 { PreConfiguredAction } from '../../types'; +import { ActionTypeId as EsIndexActionTypeId } from '../../builtin_action_types/es_index'; +import { AlertHistoryEsIndexConnectorId, AlertHistoryDefaultIndexName } from '../../../common'; + +export function getAlertHistoryEsIndex(): Readonly { + return Object.freeze({ + name: i18n.translate('xpack.actions.alertHistoryEsIndexConnector.name', { + defaultMessage: 'Alert history Elasticsearch index', + }), + actionTypeId: EsIndexActionTypeId, + id: AlertHistoryEsIndexConnectorId, + isPreconfigured: true, + config: { + index: AlertHistoryDefaultIndexName, + }, + secrets: {}, + }); +} diff --git a/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.test.ts b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.test.ts new file mode 100644 index 0000000000000..a7038d8dc62eb --- /dev/null +++ b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { ElasticsearchClient } from 'src/core/server'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import { + createAlertHistoryIndexTemplate, + getAlertHistoryIndexTemplate, +} from './create_alert_history_index_template'; + +type MockedLogger = ReturnType; + +describe('createAlertHistoryIndexTemplate', () => { + let logger: MockedLogger; + let clusterClient: DeeplyMockedKeys; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + }); + + test(`should create index template if it doesn't exist`, async () => { + // Response type for existsIndexTemplate is still TODO + clusterClient.indices.existsIndexTemplate.mockResolvedValue({ + body: false, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + await createAlertHistoryIndexTemplate({ client: clusterClient, logger }); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledWith({ + name: `kibana-alert-history-template`, + body: getAlertHistoryIndexTemplate(), + create: true, + }); + }); + + test(`shouldn't create index template if it already exists`, async () => { + // Response type for existsIndexTemplate is still TODO + clusterClient.indices.existsIndexTemplate.mockResolvedValue({ + body: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + await createAlertHistoryIndexTemplate({ client: clusterClient, logger }); + expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.ts b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.ts new file mode 100644 index 0000000000000..fe9874fb1d671 --- /dev/null +++ b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/create_alert_history_index_template.ts @@ -0,0 +1,106 @@ +/* + * 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 { ElasticsearchClient, Logger } from 'src/core/server'; +import { ALERT_HISTORY_PREFIX } from '../../../common'; +import mappings from './mappings.json'; + +export function getAlertHistoryIndexTemplate() { + return { + index_patterns: [`${ALERT_HISTORY_PREFIX}*`], + _meta: { + description: + 'System generated mapping for preconfigured alert history Elasticsearch index connector.', + }, + template: { + settings: { + number_of_shards: 1, + auto_expand_replicas: '0-1', + }, + mappings, + }, + }; +} + +async function doesIndexTemplateExist({ + client, + templateName, +}: { + client: ElasticsearchClient; + templateName: string; +}) { + let result; + try { + result = (await client.indices.existsIndexTemplate({ name: templateName })).body; + } catch (err) { + throw new Error(`error checking existence of index template: ${err.message}`); + } + + return result; +} + +async function createIndexTemplate({ + client, + template, + templateName, +}: { + client: ElasticsearchClient; + template: Record; + templateName: string; +}) { + try { + await client.indices.putIndexTemplate({ + name: templateName, + body: template, + create: true, + }); + } catch (err) { + // The error message doesn't have a type attribute we can look to guarantee it's due + // to the template already existing (only long message) so we'll check ourselves to see + // if the template now exists. This scenario would happen if you startup multiple Kibana + // instances at the same time. + const existsNow = await doesIndexTemplateExist({ client, templateName }); + if (!existsNow) { + throw new Error(`error creating index template: ${err.message}`); + } + } +} + +async function createIndexTemplateIfNotExists({ + client, + template, + templateName, +}: { + client: ElasticsearchClient; + template: Record; + templateName: string; +}) { + const indexTemplateExists = await doesIndexTemplateExist({ client, templateName }); + + if (!indexTemplateExists) { + await createIndexTemplate({ client, template, templateName }); + } +} + +export async function createAlertHistoryIndexTemplate({ + client, + logger, +}: { + client: ElasticsearchClient; + logger: Logger; +}) { + try { + const indexTemplate = getAlertHistoryIndexTemplate(); + await createIndexTemplateIfNotExists({ + client, + templateName: `${ALERT_HISTORY_PREFIX}template`, + template: indexTemplate, + }); + } catch (err) { + logger.error(`Could not initialize alert history index with mappings: ${err.message}.`); + } +} diff --git a/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/mappings.json b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/mappings.json new file mode 100644 index 0000000000000..56047f30d9489 --- /dev/null +++ b/x-pack/plugins/actions/server/preconfigured_connectors/alert_history_es_index/mappings.json @@ -0,0 +1,84 @@ +{ + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "kibana": { + "properties": { + "alert": { + "properties": { + "actionGroup": { + "type": "keyword" + }, + "actionGroupName": { + "type": "keyword" + }, + "actionSubgroup": { + "type": "keyword" + }, + "context": { + "type": "object", + "enabled": false + }, + "id": { + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword", + "meta": { + "isArray": "true" + } + }, + "message": { + "norms": false, + "type": "text" + }, + "event": { + "properties": { + "kind": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "params": { + "type": "object", + "enabled": false + }, + "space": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index b7a6750a520ea..d6f99a766ed34 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -107,7 +107,11 @@ export interface ActionType< config?: ValidatorType; secrets?: ValidatorType; }; - renderParameterTemplates?(params: Params, variables: Record): Params; + renderParameterTemplates?( + params: Params, + variables: Record, + actionId?: string + ): Params; executor: ExecutorType; } diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 9999ea6a4d3d7..2ecf540485695 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -117,6 +117,7 @@ export function createExecutionHandler< params: transformActionParams({ actionsPlugin, alertId, + alertType: alertType.id, actionTypeId: action.actionTypeId, alertName, spaceId, @@ -127,6 +128,7 @@ export function createExecutionHandler< alertActionSubgroup: actionSubgroup, context, actionParams: action.params, + actionId: action.id, state, kibanaBaseUrl, alertParams, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index a3a7e9bbd9da5..50d710f6d6b14 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -153,7 +153,7 @@ describe('Task Runner', () => { actionsClient ); taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation( - (actionTypeId, params) => params + (actionTypeId, actionId, params) => params ); }); diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts index 6379192e855d7..e325d597da145 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts @@ -34,6 +34,8 @@ test('skips non string parameters', () => { context: {}, state: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -68,6 +70,8 @@ test('missing parameters get emptied out', () => { context: {}, state: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -95,6 +99,8 @@ test('context parameters are passed to templates', () => { state: {}, context: { foo: 'fooVal' }, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -121,6 +127,8 @@ test('state parameters are passed to templates', () => { state: { bar: 'barVal' }, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -147,6 +155,8 @@ test('alertId is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -173,6 +183,8 @@ test('alertName is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -199,6 +211,8 @@ test('tags is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -225,6 +239,8 @@ test('undefined tags is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', spaceId: 'spaceId-A', alertInstanceId: '2', @@ -250,6 +266,8 @@ test('empty tags is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: [], spaceId: 'spaceId-A', @@ -276,6 +294,8 @@ test('spaceId is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -302,6 +322,8 @@ test('alertInstanceId is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -328,6 +350,8 @@ test('alertActionGroup is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -354,6 +378,8 @@ test('alertActionGroupName is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -380,6 +406,8 @@ test('rule variables are passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -408,6 +436,8 @@ test('rule alert variables are passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -436,6 +466,8 @@ test('date is passed to templates', () => { state: {}, context: {}, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -464,6 +496,8 @@ test('works recursively', () => { state: { value: 'state' }, context: { value: 'context' }, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', @@ -494,6 +528,8 @@ test('works recursively with arrays', () => { state: { value: 'state' }, context: { value: 'context' }, alertId: '1', + alertType: 'rule-type-id', + actionId: 'action-id', alertName: 'alert-name', tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index 348bf01ea874b..3f9fe9e9c59e0 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -16,6 +16,8 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../../acti interface TransformActionParamsOptions { actionsPlugin: ActionsPluginStartContract; alertId: string; + alertType: string; + actionId: string; actionTypeId: string; alertName: string; spaceId: string; @@ -34,6 +36,8 @@ interface TransformActionParamsOptions { export function transformActionParams({ actionsPlugin, alertId, + alertType, + actionId, actionTypeId, alertName, spaceId, @@ -68,6 +72,7 @@ export function transformActionParams({ rule: { id: alertId, name: alertName, + type: alertType, spaceId, tags, }, @@ -78,5 +83,10 @@ export function transformActionParams({ actionSubgroup: alertActionSubgroup, }, }; - return actionsPlugin.renderActionParameterTemplates(actionTypeId, actionParams, variables); + return actionsPlugin.renderActionParameterTemplates( + actionTypeId, + actionId, + actionParams, + variables + ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx index 00a029a9abb5a..9757653043175 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx @@ -82,32 +82,71 @@ describe('index connector validation with minimal config', () => { }); describe('action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - documents: [{ test: 1234 }], - }; + test('action params validation succeeds when action params are valid', () => { + expect( + actionTypeModel.validateParams({ + documents: [{ test: 1234 }], + }) + ).toEqual({ + errors: { + documents: [], + indexOverride: [], + }, + }); - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect( + actionTypeModel.validateParams({ + documents: [{ test: 1234 }], + indexOverride: 'kibana-alert-history-anything', + }) + ).toEqual({ errors: { documents: [], + indexOverride: [], }, }); + }); - const emptyActionParams = {}; + test('action params validation fails when action params are invalid', () => { + expect(actionTypeModel.validateParams({})).toEqual({ + errors: { + documents: ['Document is required and should be a valid JSON object.'], + indexOverride: [], + }, + }); - expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({ + expect( + actionTypeModel.validateParams({ + documents: [{}], + }) + ).toEqual({ errors: { documents: ['Document is required and should be a valid JSON object.'], + indexOverride: [], }, }); - const invalidDocumentActionParams = { - documents: [{}], - }; + expect( + actionTypeModel.validateParams({ + documents: [{}], + indexOverride: 'kibana-alert-history-', + }) + ).toEqual({ + errors: { + documents: ['Document is required and should be a valid JSON object.'], + indexOverride: ['Alert history index must contain valid suffix.'], + }, + }); - expect(actionTypeModel.validateParams(invalidDocumentActionParams)).toEqual({ + expect( + actionTypeModel.validateParams({ + documents: [{}], + indexOverride: 'this.is-a_string', + }) + ).toEqual({ errors: { documents: ['Document is required and should be a valid JSON object.'], + indexOverride: ['Alert history index must begin with "kibana-alert-history-".'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx index bc09e5abe1120..f4b8284c8cfa6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx @@ -11,6 +11,7 @@ import { ActionTypeModel, GenericValidationResult, ConnectorValidationResult, + ALERT_HISTORY_PREFIX, } from '../../../../types'; import { EsIndexActionConnector, EsIndexConfig, IndexActionParams } from '../types'; @@ -56,6 +57,7 @@ export function getActionType(): ActionTypeModel => { const errors = { documents: new Array(), + indexOverride: new Array(), }; const validationResult = { errors }; if (!actionParams.documents?.length || Object.keys(actionParams.documents[0]).length === 0) { @@ -68,6 +70,32 @@ export function getActionType(): ActionTypeModel { - test('all params fields is rendered', () => { + test('all params fields are rendered correctly when params are undefined', () => { + const actionParams = { + documents: undefined, + }; + const wrapper = mountWithIntl( + {}} + index={0} + actionConnector={actionConnector} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} + /> + ); + expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(``); + expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeFalsy(); + }); + + test('all params fields are rendered when document params are defined', () => { const actionParams = { documents: [{ test: 123 }], }; @@ -22,6 +73,7 @@ describe('IndexParamsFields renders', () => { errors={{ index: [] }} editAction={() => {}} index={0} + actionConnector={actionConnector} messageVariables={[ { name: 'myVar', @@ -35,5 +87,76 @@ describe('IndexParamsFields renders', () => { "test": 123 }`); expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeFalsy(); + }); + + test('all params fields are rendered correctly for preconfigured alert history connector when params are undefined', () => { + const actionParams = { + documents: undefined, + }; + const wrapper = mountWithIntl( + {}} + index={0} + actionConnector={preconfiguredActionConnector} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} + /> + ); + expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').first().prop('value')).toBe( + 'default' + ); + expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeTruthy(); + }); + + test('all params fields are rendered correctly for preconfigured alert history connector when params are defined', async () => { + const actionParams = { + documents: undefined, + indexOverride: 'kibana-alert-history-not-the-default', + }; + const wrapper = mountWithIntl( + {}} + index={0} + actionConnector={preconfiguredActionConnector} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} + /> + ); + expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').first().prop('value')).toBe( + 'not-the-default' + ); + expect(wrapper.find('[data-test-subj="preconfiguredDocumentToIndex"]').length > 0).toBeTruthy(); + + wrapper.find('EuiLink[data-test-subj="resetDefaultIndex"]').simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="preconfiguredIndexToUse"]').first().prop('value')).toBe( + 'default' + ); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index c65c76ee6916e..6973cdcc7a088 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -5,11 +5,25 @@ * 2.0. */ -import React from 'react'; -import { EuiLink } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { + EuiIcon, + EuiText, + EuiCodeBlock, + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ActionParamsProps } from '../../../../types'; +import { + ActionParamsProps, + AlertHistoryEsIndexConnectorId, + AlertHistoryDocumentTemplate, + AlertHistoryDefaultIndexName, + ALERT_HISTORY_PREFIX, +} from '../../../../types'; import { IndexActionParams } from '.././types'; import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; import { useKibana } from '../../../../common/lib/kibana'; @@ -20,38 +34,152 @@ export const IndexParamsFields = ({ editAction, messageVariables, errors, + actionConnector, }: ActionParamsProps) => { const { docLinks } = useKibana().services; - const { documents } = actionParams; + const { documents, indexOverride } = actionParams; + + const defaultAlertHistoryIndexSuffix = AlertHistoryDefaultIndexName.replace( + ALERT_HISTORY_PREFIX, + '' + ); + + const getDocumentToIndex = (doc: Array> | undefined) => + doc && doc.length > 0 ? ((doc[0] as unknown) as string) : undefined; + + const [documentToIndex, setDocumentToIndex] = useState( + getDocumentToIndex(documents) + ); + const [alertHistoryIndexSuffix, setAlertHistoryIndexSuffix] = useState( + indexOverride ? indexOverride.replace(ALERT_HISTORY_PREFIX, '') : defaultAlertHistoryIndexSuffix + ); + const [usePreconfiguredSchema, setUsePreconfiguredSchema] = useState(false); + + useEffect(() => { + setDocumentToIndex(getDocumentToIndex(documents)); + }, [documents]); + + useEffect(() => { + if (actionConnector?.id === AlertHistoryEsIndexConnectorId) { + setUsePreconfiguredSchema(true); + editAction('documents', [JSON.stringify(AlertHistoryDocumentTemplate)], index); + setDocumentToIndex(JSON.stringify(AlertHistoryDocumentTemplate)); + } else { + setUsePreconfiguredSchema(false); + editAction('documents', undefined, index); + setDocumentToIndex(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector?.id]); const onDocumentsChange = (updatedDocuments: string) => { try { const documentsJSON = JSON.parse(updatedDocuments); editAction('documents', [documentsJSON], index); + setDocumentToIndex(updatedDocuments); } catch (e) { // set document as empty to turn on the validation for non empty valid JSON object editAction('documents', [{}], index); + setDocumentToIndex(undefined); } }; - return ( + const documentsFieldLabel = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', + { + defaultMessage: 'Document to index', + } + ); + + const resetDefaultIndex = + indexOverride && indexOverride !== AlertHistoryDefaultIndexName ? ( + + { + editAction('indexOverride', AlertHistoryDefaultIndexName, index); + setAlertHistoryIndexSuffix(defaultAlertHistoryIndexSuffix); + }} + > + + + + + ) : ( + <> + ); + + const preconfiguredDocumentSchema = ( + <> + 0} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.preconfiguredIndex', + { + defaultMessage: 'Elasticsearch index', + } + )} + labelAppend={resetDefaultIndex} + helpText={ + <> + + + + + + } + > + { + editAction('indexOverride', `${ALERT_HISTORY_PREFIX}${e.target.value}`, index); + setAlertHistoryIndexSuffix(e.target.value); + }} + /> + + + + + {JSON.stringify(AlertHistoryDocumentTemplate, null, 2)} + + + + ); + + const jsonDocumentEditor = ( 0 - ? ((documents[0] as unknown) as string) - : undefined + : documentToIndex } - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', - { - defaultMessage: 'Document to index', - } - )} + label={documentsFieldLabel} aria-label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel', { @@ -69,15 +197,15 @@ export const IndexParamsFields = ({ } onBlur={() => { - if ( - !(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined) - ) { + if (!documentToIndex) { // set document as empty to turn on the validation for non empty valid JSON object onDocumentsChange('{}'); } }} /> ); + + return usePreconfiguredSchema ? preconfiguredDocumentSchema : jsonDocumentEditor; }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 8a1b2bfb4ac22..d94cdde349dc7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -42,6 +42,7 @@ export interface PagerDutyActionParams { export interface IndexActionParams { documents: Array>; + indexOverride?: string; } export enum ServerLogLevelOptions { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 435e4c5637ee5..1414242358d58 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -32,6 +32,10 @@ describe('transformActionVariables', () => { "description": "The tags of the rule.", "name": "rule.tags", }, + Object { + "description": "The type of rule.", + "name": "rule.type", + }, Object { "description": "The date the rule scheduled the action.", "name": "date", @@ -127,6 +131,10 @@ describe('transformActionVariables', () => { "description": "The tags of the rule.", "name": "rule.tags", }, + Object { + "description": "The type of rule.", + "name": "rule.type", + }, Object { "description": "The date the rule scheduled the action.", "name": "date", @@ -230,6 +238,10 @@ describe('transformActionVariables', () => { "description": "The tags of the rule.", "name": "rule.tags", }, + Object { + "description": "The type of rule.", + "name": "rule.type", + }, Object { "description": "The date the rule scheduled the action.", "name": "date", @@ -336,6 +348,10 @@ describe('transformActionVariables', () => { "description": "The tags of the rule.", "name": "rule.tags", }, + Object { + "description": "The type of rule.", + "name": "rule.type", + }, Object { "description": "The date the rule scheduled the action.", "name": "date", @@ -460,6 +476,10 @@ describe('transformActionVariables', () => { "description": "The tags of the rule.", "name": "rule.tags", }, + Object { + "description": "The type of rule.", + "name": "rule.type", + }, Object { "description": "The date the rule scheduled the action.", "name": "date", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index 29f2b277c97a3..9722cc42ed396 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -26,6 +26,7 @@ export enum AlertProvidedActionVariables { ruleName = 'rule.name', ruleSpaceId = 'rule.spaceId', ruleTags = 'rule.tags', + ruleType = 'rule.type', date = 'date', alertId = 'alert.id', alertActionGroup = 'alert.actionGroup', @@ -83,6 +84,13 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] { }), }); + result.push({ + name: AlertProvidedActionVariables.ruleType, + description: i18n.translate('xpack.triggersActionsUI.actionVariables.ruleTypeLabel', { + defaultMessage: 'The type of rule.', + }), + }); + result.push({ name: AlertProvidedActionVariables.date, description: i18n.translate('xpack.triggersActionsUI.actionVariables.dateLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index cf2dda203bb2d..1fd031cda6d96 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -10,7 +10,13 @@ import type { DocLinksStart } from 'kibana/public'; import { ComponentType } from 'react'; import { ChartsPluginSetup } from 'src/plugins/charts/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { ActionType } from '../../actions/common'; +import { + ActionType, + AlertHistoryEsIndexConnectorId, + AlertHistoryDocumentTemplate, + ALERT_HISTORY_PREFIX, + AlertHistoryDefaultIndexName, +} from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; import { ActionGroup, @@ -45,7 +51,13 @@ export { AlertNotifyWhenType, AlertTypeParams, }; -export { ActionType }; +export { + ActionType, + AlertHistoryEsIndexConnectorId, + AlertHistoryDocumentTemplate, + AlertHistoryDefaultIndexName, + ALERT_HISTORY_PREFIX, +}; export type ActionTypeIndex = Record; export type AlertTypeIndex = Map; diff --git a/x-pack/plugins/uptime/public/state/api/alert_actions.ts b/x-pack/plugins/uptime/public/state/api/alert_actions.ts index 17b3354b666c4..592fd56984707 100644 --- a/x-pack/plugins/uptime/public/state/api/alert_actions.ts +++ b/x-pack/plugins/uptime/public/state/api/alert_actions.ts @@ -84,6 +84,7 @@ function getIndexActionParams(): IndexActionParams { observerLocation: '{{state.observerLocation}}', }, ], + indexOverride: null, }; } diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index beb639eb46334..6a0ab54087844 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -20,6 +20,7 @@ interface CreateTestConfigOptions { enableActionsProxy: boolean; rejectUnauthorized?: boolean; publicBaseUrl?: boolean; + preconfiguredAlertHistoryEsIndex?: boolean; } // test.not-enabled is specifically not enabled @@ -47,6 +48,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) disabledPlugins = [], ssl = false, rejectUnauthorized = true, + preconfiguredAlertHistoryEsIndex = false, } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -119,6 +121,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...actionsProxyUrl, '--xpack.eventLog.logEntries=true', + `--xpack.actions.preconfiguredAlertHistoryEsIndex=${preconfiguredAlertHistoryEsIndex}`, `--xpack.actions.preconfigured=${JSON.stringify({ 'my-slack1': { actionTypeId: '.slack', diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index c397a2659557f..49d5f52869b89 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -13,4 +13,5 @@ export default createTestConfig('spaces_only', { license: 'trial', enableActionsProxy: false, rejectUnauthorized: false, + preconfiguredAlertHistoryEsIndex: true, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts new file mode 100644 index 0000000000000..cf8a0f99d4394 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts @@ -0,0 +1,165 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getTestAlertData, ObjectRemover } from '../../../../common/lib'; +import { AlertHistoryDefaultIndexName } from '../../../../../../plugins/actions/common'; + +const ALERT_HISTORY_OVERRIDE_INDEX = 'kibana-alert-history-not-the-default'; + +// eslint-disable-next-line import/no-default-export +export default function preconfiguredAlertHistoryConnectorTests({ + getService, +}: FtrProviderContext) { + const es = getService('legacyEs'); + const supertest = getService('supertest'); + const retry = getService('retry'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + + describe('preconfigured alert history connector', () => { + const spaceId = 'default'; + const ruleTypeId = 'test.patternFiring'; + const alertId = 'instance'; + + function getTestData(params = {}) { + return getTestAlertData({ + rule_type_id: ruleTypeId, + schedule: { interval: '1s' }, + params: { + pattern: { [alertId]: new Array(100).fill(true) }, + }, + actions: [ + { + group: 'default', + id: 'preconfigured-alert-history-es-index', + params, + }, + ], + }); + } + + const objectRemover = new ObjectRemover(supertest); + beforeEach(() => { + esDeleteAllIndices(AlertHistoryDefaultIndexName); + esDeleteAllIndices(ALERT_HISTORY_OVERRIDE_INDEX); + }); + after(() => objectRemover.removeAll()); + + it('should index document with preconfigured schema', async () => { + const testRuleData = getTestData({ + documents: [{}], + }); + const response = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(testRuleData); + expect(response.status).to.eql(200); + objectRemover.add(spaceId, response.body.id, 'rule', 'alerting'); + + // Wait for alert to be active + await waitForStatus(response.body.id, new Set(['active'])); + + await retry.try(async () => { + const result = await es.search({ + index: AlertHistoryDefaultIndexName, + }); + const indexedItems = result.hits.hits; + expect(indexedItems.length).to.eql(1); + + const indexedDoc = indexedItems[0]._source; + + const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(indexedDoc['@timestamp']).to.match(timestampPattern); + expect(indexedDoc.tags).to.eql(testRuleData.tags); + expect(indexedDoc.rule.name).to.eql(testRuleData.name); + expect(indexedDoc.rule.params[ruleTypeId.replace('.', '__')]).to.eql(testRuleData.params); + expect(indexedDoc.rule.space).to.eql(spaceId); + expect(indexedDoc.rule.type).to.eql(ruleTypeId); + expect(indexedDoc.kibana.alert.id).to.eql(alertId); + expect(indexedDoc.kibana.alert.context[ruleTypeId.replace('.', '__')] != null).to.eql(true); + expect(indexedDoc.kibana.alert.actionGroup).to.eql('default'); + expect(indexedDoc.kibana.alert.actionGroupName).to.eql('Default'); + }); + }); + + it('should index document with preconfigured schema when indexOverride is defined', async () => { + const testRuleData = getTestData({ + documents: [{}], + indexOverride: ALERT_HISTORY_OVERRIDE_INDEX, + }); + const response = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(testRuleData); + expect(response.status).to.eql(200); + objectRemover.add(spaceId, response.body.id, 'rule', 'alerting'); + + // Wait for alert to be active + await waitForStatus(response.body.id, new Set(['active'])); + + await retry.try(async () => { + const result = await es.search({ + index: ALERT_HISTORY_OVERRIDE_INDEX, + }); + const indexedItems = result.hits.hits; + expect(indexedItems.length).to.eql(1); + + const indexedDoc = indexedItems[0]._source; + + const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(indexedDoc['@timestamp']).to.match(timestampPattern); + expect(indexedDoc.tags).to.eql(testRuleData.tags); + expect(indexedDoc.rule.name).to.eql(testRuleData.name); + expect(indexedDoc.rule.params[ruleTypeId.replace('.', '__')]).to.eql(testRuleData.params); + expect(indexedDoc.rule.space).to.eql(spaceId); + expect(indexedDoc.rule.type).to.eql(ruleTypeId); + expect(indexedDoc.kibana.alert.id).to.eql(alertId); + expect(indexedDoc.kibana.alert.context[ruleTypeId.replace('.', '__')] != null).to.eql(true); + expect(indexedDoc.kibana.alert.actionGroup).to.eql('default'); + expect(indexedDoc.kibana.alert.actionGroupName).to.eql('Default'); + }); + }); + }); + + const WaitForStatusIncrement = 500; + + async function waitForStatus( + id: string, + statuses: Set, + waitMillis: number = 10000 + ): Promise> { + if (waitMillis < 0) { + expect().fail(`waiting for alert ${id} statuses ${Array.from(statuses)} timed out`); + } + + const response = await supertest.get(`/api/alerts/alert/${id}`); + expect(response.status).to.eql(200); + + const { executionStatus } = response.body || {}; + const { status } = executionStatus || {}; + + const message = `waitForStatus(${Array.from(statuses)}): got ${JSON.stringify( + executionStatus + )}`; + + if (statuses.has(status)) { + return executionStatus; + } + + // eslint-disable-next-line no-console + console.log(`${message}, retrying`); + + await delay(WaitForStatusIncrement); + return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement); + } + + async function delay(millis: number): Promise { + await new Promise((resolve) => setTimeout(resolve, millis)); + } +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index 531df9d4ed19f..08241f2ad8e22 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -36,6 +36,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connectors`).expect(200, [ + { + id: 'preconfigured-alert-history-es-index', + name: 'Alert history Elasticsearch index', + connector_type_id: '.index', + is_preconfigured: true, + referenced_by_count: 0, + }, { id: createdAction.id, is_preconfigured: false, @@ -95,6 +102,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/actions/connectors`).expect(200, [ + { + id: 'preconfigured-alert-history-es-index', + name: 'Alert history Elasticsearch index', + connector_type_id: '.index', + is_preconfigured: true, + referenced_by_count: 0, + }, { id: 'preconfigured-es-index-action', is_preconfigured: true, @@ -145,6 +159,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/actions`).expect(200, [ + { + id: 'preconfigured-alert-history-es-index', + name: 'Alert history Elasticsearch index', + actionTypeId: '.index', + isPreconfigured: true, + referencedByCount: 0, + }, { id: createdAction.id, isPreconfigured: false, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index d5056508e5de9..43f442c131626 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -23,6 +23,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./execute')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/webhook')); + loadTestFile(require.resolve('./builtin_action_types/preconfigured_alert_history_connector')); loadTestFile(require.resolve('./type_not_enabled')); // note that this test will destroy existing spaces diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 5dd1890e240a4..91a349e1bf44a 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -66,6 +66,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + `--xpack.actions.preconfiguredAlertHistoryEsIndex=false`, `--xpack.actions.preconfigured=${JSON.stringify({ 'my-slack1': { actionTypeId: '.slack',