From c2fc58310a1b035c5d5c8224754a6382b7473567 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 14 Jan 2021 12:00:56 +0100 Subject: [PATCH] [Uptime] simple monitor status alert fix for page duty and other connectors (#87460) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../uptime/common/constants/rest_api.ts | 1 + .../runtime_types/alerts/status_check.ts | 1 + .../settings/add_connector_flyout.tsx | 22 ++- .../settings/alert_defaults_form.tsx | 15 +- .../public/components/settings/types.ts | 27 ++++ .../lib/alert_types/monitor_status.test.ts | 10 +- .../uptime/public/state/api/alert_actions.ts | 144 ++++++++++++++++++ .../plugins/uptime/public/state/api/alerts.ts | 43 ++++-- 8 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/settings/types.ts create mode 100644 x-pack/plugins/uptime/public/state/api/alert_actions.ts diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index be1f498c2e75..6916a5ea4788 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -29,4 +29,5 @@ export enum API_URLS { CREATE_ALERT = '/api/alerts/alert', ALERT = '/api/alerts/alert/', ALERTS_FIND = '/api/alerts/_find', + ACTION_TYPES = '/api/actions/list_action_types', } diff --git a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts index 971a9f51bfae..8996bad8d4f0 100644 --- a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts +++ b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts @@ -26,6 +26,7 @@ export const AtomicStatusCheckParamsType = t.intersection([ filters: StatusCheckFiltersType, shouldCheckStatus: t.boolean, isAutoGenerated: t.boolean, + shouldCheckAvailability: t.boolean, }), ]); diff --git a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx index 2f0faebdf248..ce2d0f6ace7e 100644 --- a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx @@ -11,6 +11,10 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public'; import { getConnectorsAction } from '../../state/alerts/alerts'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useFetcher } from '../../../../observability/public'; +import { fetchActionTypes } from '../../state/api/alerts'; + +import { ActionTypeId } from './types'; interface Props { focusInput: () => void; @@ -20,6 +24,17 @@ interface KibanaDeps { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } +export const ALLOWED_ACTION_TYPES: ActionTypeId[] = [ + '.slack', + '.pagerduty', + '.server-log', + '.index', + '.teams', + '.servicenow', + '.jira', + '.webhook', +]; + export const AddConnectorFlyout = ({ focusInput }: Props) => { const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); const { @@ -30,6 +45,8 @@ export const AddConnectorFlyout = ({ focusInput }: Props) => { const dispatch = useDispatch(); + const { data: actionTypes } = useFetcher(() => fetchActionTypes(), []); + const ConnectorAddFlyout = useMemo( () => getAddConnectorFlyout({ @@ -39,9 +56,12 @@ export const AddConnectorFlyout = ({ focusInput }: Props) => { setAddFlyoutVisibility(false); focusInput(); }, + actionTypes: (actionTypes ?? []).filter((actionType) => + ALLOWED_ACTION_TYPES.includes(actionType.id as ActionTypeId) + ), }), // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [actionTypes] ); return ( diff --git a/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx index 9c2bd3e86b46..68e9a8297cf3 100644 --- a/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx +++ b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx @@ -19,12 +19,13 @@ import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { SettingsFormProps } from '../../pages/settings'; import { connectorsSelector } from '../../state/alerts/alerts'; -import { AddConnectorFlyout } from './add_connector_flyout'; +import { AddConnectorFlyout, ALLOWED_ACTION_TYPES } from './add_connector_flyout'; import { useGetUrlParams, useUrlParams } from '../../hooks'; import { alertFormI18n } from './translations'; import { useInitApp } from '../../hooks/use_init_app'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public/'; +import { ActionTypeId } from './types'; type ConnectorOption = EuiComboBoxOptionOption; @@ -88,11 +89,13 @@ export const AlertDefaultsForm: React.FC = ({ ); }; - const options = (data ?? []).map((connectorAction) => ({ - value: connectorAction.id, - label: connectorAction.name, - 'data-test-subj': connectorAction.name, - })); + const options = (data ?? []) + .filter((action) => ALLOWED_ACTION_TYPES.includes(action.actionTypeId as ActionTypeId)) + .map((connectorAction) => ({ + value: connectorAction.id, + label: connectorAction.name, + 'data-test-subj': connectorAction.name, + })); const renderOption = (option: ConnectorOption) => { const { label, value } = option; diff --git a/x-pack/plugins/uptime/public/components/settings/types.ts b/x-pack/plugins/uptime/public/components/settings/types.ts new file mode 100644 index 000000000000..faa1c7e72e47 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IndexActionTypeId, + JiraActionTypeId, + PagerDutyActionTypeId, + ServerLogActionTypeId, + ServiceNowActionTypeId, + SlackActionTypeId, + TeamsActionTypeId, + WebhookActionTypeId, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../actions/server/builtin_action_types'; + +export type ActionTypeId = + | typeof SlackActionTypeId + | typeof PagerDutyActionTypeId + | typeof ServerLogActionTypeId + | typeof IndexActionTypeId + | typeof TeamsActionTypeId + | typeof ServiceNowActionTypeId + | typeof JiraActionTypeId + | typeof WebhookActionTypeId; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts index 7171aa482863..86672efc61eb 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts @@ -26,9 +26,9 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number", - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string", ], }, } @@ -151,7 +151,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } @@ -164,7 +164,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } diff --git a/x-pack/plugins/uptime/public/state/api/alert_actions.ts b/x-pack/plugins/uptime/public/state/api/alert_actions.ts new file mode 100644 index 000000000000..0e8781e5937d --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/alert_actions.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NewAlertParams } from './alerts'; +import { AlertAction } from '../../../../triggers_actions_ui/public'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { MonitorStatusTranslations } from '../../../common/translations'; +import { + IndexActionParams, + PagerDutyActionParams, + ServerLogActionParams, + ServiceNowActionParams, + JiraActionParams, + WebhookActionParams, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../actions/server'; +import { ActionTypeId } from '../../components/settings/types'; + +export const SLACK_ACTION_ID: ActionTypeId = '.slack'; +export const PAGER_DUTY_ACTION_ID: ActionTypeId = '.pagerduty'; +export const SERVER_LOG_ACTION_ID: ActionTypeId = '.server-log'; +export const INDEX_ACTION_ID: ActionTypeId = '.index'; +export const TEAMS_ACTION_ID: ActionTypeId = '.teams'; +export const SERVICE_NOW_ACTION_ID: ActionTypeId = '.servicenow'; +export const JIRA_ACTION_ID: ActionTypeId = '.jira'; +export const WEBHOOK_ACTION_ID: ActionTypeId = '.webhook'; + +const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; + +export function populateAlertActions({ defaultActions, monitorId, monitorName }: NewAlertParams) { + const actions: AlertAction[] = []; + defaultActions.forEach((aId) => { + const action: AlertAction = { + id: aId.id, + actionTypeId: aId.actionTypeId, + group: MONITOR_STATUS.id, + params: {}, + }; + switch (aId.actionTypeId) { + case PAGER_DUTY_ACTION_ID: + action.params = getPagerDutyActionParams(monitorId); + break; + case SERVER_LOG_ACTION_ID: + action.params = getServerLogActionParams(); + break; + case INDEX_ACTION_ID: + action.params = getIndexActionParams(); + break; + case SERVICE_NOW_ACTION_ID: + action.params = getServiceNowActionParams(); + break; + case JIRA_ACTION_ID: + action.params = getJiraActionParams(); + break; + case WEBHOOK_ACTION_ID: + action.params = getWebhookActionParams(); + break; + case SLACK_ACTION_ID: + case TEAMS_ACTION_ID: + default: + action.params = { + message: MonitorStatusTranslations.defaultActionMessage, + }; + } + + actions.push(action); + }); + + return actions; +} + +function getIndexActionParams(): IndexActionParams { + return { + documents: [ + { + monitorName: '{{state.monitorName}}', + monitorUrl: '{{{state.monitorUrl}}}', + statusMessage: '{{state.statusMessage}}', + latestErrorMessage: '{{{state.latestErrorMessage}}}', + observerLocation: '{{state.observerLocation}}', + }, + ], + }; +} + +function getServerLogActionParams(): ServerLogActionParams { + return { + level: 'warn', + message: MonitorStatusTranslations.defaultActionMessage, + }; +} + +function getWebhookActionParams(): WebhookActionParams { + return { + body: MonitorStatusTranslations.defaultActionMessage, + }; +} + +function getPagerDutyActionParams(monitorId: string): PagerDutyActionParams { + return { + dedupKey: monitorId + MONITOR_STATUS.id, + eventAction: 'trigger', + severity: 'error', + summary: MonitorStatusTranslations.defaultActionMessage, + }; +} + +function getServiceNowActionParams(): ServiceNowActionParams { + return { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: MonitorStatusTranslations.defaultActionMessage, + description: MonitorStatusTranslations.defaultActionMessage, + impact: '2', + severity: '2', + urgency: '2', + externalId: null, + }, + comments: [], + }, + }; +} + +function getJiraActionParams(): JiraActionParams { + return { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: MonitorStatusTranslations.defaultActionMessage, + externalId: null, + description: MonitorStatusTranslations.defaultActionMessage, + issueType: null, + priority: '2', + labels: null, + parent: null, + }, + comments: [], + }, + }; +} diff --git a/x-pack/plugins/uptime/public/state/api/alerts.ts b/x-pack/plugins/uptime/public/state/api/alerts.ts index 9d4dd3a1253c..dd78be5d08ea 100644 --- a/x-pack/plugins/uptime/public/state/api/alerts.ts +++ b/x-pack/plugins/uptime/public/state/api/alerts.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ACTION_GROUP_DEFINITIONS, CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; +import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { apiService } from './utils'; import { ActionConnector } from '../alerts/alerts'; import { AlertsResult, MonitorIdParam } from '../actions/types'; -import { AlertAction } from '../../../../triggers_actions_ui/public'; +import { ActionType, AlertAction } from '../../../../triggers_actions_ui/public'; import { API_URLS } from '../../../common/constants'; -import { MonitorStatusTranslations } from '../../../common/translations'; import { Alert, AlertTypeParams } from '../../../../alerts/common'; +import { AtomicStatusCheckParams } from '../../../common/runtime_types/alerts'; -const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; +import { populateAlertActions } from './alert_actions'; const UPTIME_AUTO_ALERT = 'UPTIME_AUTO'; @@ -28,24 +28,28 @@ export interface NewAlertParams extends AlertTypeParams { defaultActions: ActionConnector[]; } +type NewMonitorStatusAlert = Omit< + Alert, + | 'id' + | 'createdBy' + | 'updatedBy' + | 'createdAt' + | 'updatedAt' + | 'apiKey' + | 'apiKeyOwner' + | 'muteAll' + | 'mutedInstanceIds' + | 'executionStatus' +>; + export const createAlert = async ({ defaultActions, monitorId, monitorName, }: NewAlertParams): Promise => { - const actions: AlertAction[] = []; - defaultActions.forEach((aId) => { - actions.push({ - id: aId.id, - actionTypeId: aId.actionTypeId, - group: MONITOR_STATUS.id, - params: { - message: MonitorStatusTranslations.defaultActionMessage, - }, - }); - }); + const actions: AlertAction[] = populateAlertActions({ defaultActions, monitorId, monitorName }); - const data = { + const data: NewMonitorStatusAlert = { actions, params: { numTimes: 1, @@ -60,8 +64,11 @@ export const createAlert = async ({ consumer: 'uptime', alertTypeId: CLIENT_ALERT_TYPES.MONITOR_STATUS, schedule: { interval: '1m' }, + notifyWhen: 'onActionGroupChange', tags: [UPTIME_AUTO_ALERT], name: `${monitorName} (Simple status alert)`, + enabled: true, + throttle: null, }; return await apiService.post(API_URLS.CREATE_ALERT, data); @@ -99,3 +106,7 @@ export const fetchAlertRecords = async ({ export const disableAlertById = async ({ alertId }: { alertId: string }) => { return await apiService.delete(API_URLS.ALERT + alertId); }; + +export const fetchActionTypes = async (): Promise => { + return await apiService.get(API_URLS.ACTION_TYPES); +};