[Uptime] simple monitor status alert fix for page duty and other connectors (#87460)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2021-01-14 12:00:56 +01:00 committed by GitHub
parent 6ec868b54e
commit c2fc58310a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 235 additions and 28 deletions

View file

@ -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',
}

View file

@ -26,6 +26,7 @@ export const AtomicStatusCheckParamsType = t.intersection([
filters: StatusCheckFiltersType,
shouldCheckStatus: t.boolean,
isAutoGenerated: t.boolean,
shouldCheckAvailability: t.boolean,
}),
]);

View file

@ -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<boolean>(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 (

View file

@ -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<string>;
@ -88,11 +89,13 @@ export const AlertDefaultsForm: React.FC<SettingsFormProps> = ({
);
};
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;

View file

@ -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;

View file

@ -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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
],
},
}

View file

@ -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: [],
},
};
}

View file

@ -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<AtomicStatusCheckParams>,
| 'id'
| 'createdBy'
| 'updatedBy'
| 'createdAt'
| 'updatedAt'
| 'apiKey'
| 'apiKeyOwner'
| 'muteAll'
| 'mutedInstanceIds'
| 'executionStatus'
>;
export const createAlert = async ({
defaultActions,
monitorId,
monitorName,
}: NewAlertParams): Promise<Alert> => {
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<ActionType[]> => {
return await apiService.get(API_URLS.ACTION_TYPES);
};