[Uptime] Duration Anomaly Alert (#71208)
This commit is contained in:
parent
8f8736cce8
commit
981d678e42
|
@ -25,7 +25,14 @@ export function getResultsServiceProvider({
|
|||
}: SharedServicesChecks): ResultsServiceProvider {
|
||||
return {
|
||||
resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) {
|
||||
const hasMlCapabilities = getHasMlCapabilities(request);
|
||||
// Uptime is using this service in anomaly alert, kibana alerting doesn't provide request object
|
||||
// So we are adding a dummy request for now
|
||||
// TODO: Remove this once kibana alerting provides request object
|
||||
const hasMlCapabilities =
|
||||
request.params !== 'DummyKibanaRequest'
|
||||
? getHasMlCapabilities(request)
|
||||
: (_caps: string[]) => Promise.resolve();
|
||||
|
||||
const { getAnomaliesTableData } = resultsServiceProvider(mlClusterClient);
|
||||
return {
|
||||
async getAnomaliesTableData(...args) {
|
||||
|
|
|
@ -20,9 +20,14 @@ export const ACTION_GROUP_DEFINITIONS: ActionGroupDefinitions = {
|
|||
id: 'xpack.uptime.alerts.actionGroups.tls',
|
||||
name: 'Uptime TLS Alert',
|
||||
},
|
||||
DURATION_ANOMALY: {
|
||||
id: 'xpack.uptime.alerts.actionGroups.durationAnomaly',
|
||||
name: 'Uptime Duration Anomaly',
|
||||
},
|
||||
};
|
||||
|
||||
export const CLIENT_ALERT_TYPES = {
|
||||
MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus',
|
||||
TLS: 'xpack.uptime.alerts.tls',
|
||||
DURATION_ANOMALY: 'xpack.uptime.alerts.durationAnomaly',
|
||||
};
|
||||
|
|
|
@ -24,4 +24,6 @@ export enum API_URLS {
|
|||
ML_DELETE_JOB = `/api/ml/jobs/delete_jobs`,
|
||||
ML_CAPABILITIES = '/api/ml/ml_capabilities',
|
||||
ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`,
|
||||
ALERT = '/api/alerts/alert/',
|
||||
ALERTS_FIND = '/api/alerts/_find',
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getMLJobId } from '../ml_anomaly';
|
||||
import { getMLJobId } from '../ml';
|
||||
|
||||
describe('ML Anomaly API', () => {
|
||||
it('it generates a lowercase job id', async () => {
|
|
@ -6,3 +6,5 @@
|
|||
|
||||
export * from './combine_filters_and_user_search';
|
||||
export * from './stringify_kueries';
|
||||
|
||||
export { getMLJobId } from './ml';
|
||||
|
|
27
x-pack/plugins/uptime/common/lib/ml.ts
Normal file
27
x-pack/plugins/uptime/common/lib/ml.ts
Normal 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 { ML_JOB_ID } from '../constants';
|
||||
|
||||
export const getJobPrefix = (monitorId: string) => {
|
||||
// ML App doesn't support upper case characters in job name
|
||||
// Also Spaces and the characters / ? , " < > | * are not allowed
|
||||
// so we will replace all special chars with _
|
||||
|
||||
const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase();
|
||||
|
||||
// ML Job ID can't be greater than 64 length, so will be substring it, and hope
|
||||
// At such big length, there is minimum chance of having duplicate monitor id
|
||||
// Subtracting ML_JOB_ID constant as well
|
||||
const postfix = '_' + ML_JOB_ID;
|
||||
|
||||
if ((prefix + postfix).length > 64) {
|
||||
return prefix.substring(0, 64 - postfix.length) + '_';
|
||||
}
|
||||
return prefix + '_';
|
||||
};
|
||||
|
||||
export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`;
|
|
@ -2,7 +2,7 @@
|
|||
"configPath": ["xpack", "uptime"],
|
||||
"id": "uptime",
|
||||
"kibanaVersion": "kibana",
|
||||
"optionalPlugins": ["capabilities", "data", "home", "observability"],
|
||||
"optionalPlugins": ["capabilities", "data", "home", "observability", "ml"],
|
||||
"requiredPlugins": [
|
||||
"alerts",
|
||||
"embeddable",
|
||||
|
|
|
@ -11,8 +11,8 @@ import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
|||
|
||||
describe('Manage ML Job', () => {
|
||||
it('shallow renders without errors', () => {
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue(true);
|
||||
jest.spyOn(redux, 'useSelector').mockReturnValue(true);
|
||||
jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn());
|
||||
|
||||
const wrapper = shallowWithRouter(
|
||||
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
|
||||
|
@ -21,8 +21,8 @@ describe('Manage ML Job', () => {
|
|||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue(true);
|
||||
jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn());
|
||||
jest.spyOn(redux, 'useSelector').mockReturnValue(true);
|
||||
|
||||
const wrapper = renderWithRouter(
|
||||
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import * as labels from './translations';
|
||||
|
||||
interface Props {
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmAlertDeletion: React.FC<Props> = ({ onConfirm, onCancel }) => {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={labels.DISABLE_ANOMALY_ALERT}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
data-test-subj="uptimeMLAlertDeleteConfirmModel"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorDetails.ml.confirmAlertDeleteMessage"
|
||||
defaultMessage="Are you sure you want to delete the alert for anomalies?"
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -7,7 +7,8 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
|
||||
import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants';
|
||||
import {
|
||||
canDeleteMLJobSelector,
|
||||
hasMLJobSelector,
|
||||
|
@ -18,6 +19,10 @@ import * as labels from './translations';
|
|||
import { getMLJobLinkHref } from './ml_job_link';
|
||||
import { useGetUrlParams } from '../../../hooks';
|
||||
import { useMonitorId } from '../../../hooks';
|
||||
import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../state/actions';
|
||||
import { useAnomalyAlert } from './use_anomaly_alert';
|
||||
import { ConfirmAlertDeletion } from './confirm_alert_delete';
|
||||
import { deleteAlertAction } from '../../../state/actions/alerts';
|
||||
|
||||
interface Props {
|
||||
hasMLJob: boolean;
|
||||
|
@ -40,6 +45,15 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
|
|||
|
||||
const monitorId = useMonitorId();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const anomalyAlert = useAnomalyAlert();
|
||||
|
||||
const [isConfirmAlertDeleteOpen, setIsConfirmAlertDeleteOpen] = useState(false);
|
||||
|
||||
const deleteAnomalyAlert = () =>
|
||||
dispatch(deleteAlertAction.get({ alertId: anomalyAlert?.id as string }));
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={hasMLJob ? 'uptimeManageMLJobBtn' : 'uptimeEnableAnomalyBtn'}
|
||||
|
@ -68,6 +82,21 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
|
|||
}),
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
name: anomalyAlert ? labels.DISABLE_ANOMALY_ALERT : labels.ENABLE_ANOMALY_ALERT,
|
||||
'data-test-subj': anomalyAlert
|
||||
? 'uptimeDisableAnomalyAlertBtn'
|
||||
: 'uptimeEnableAnomalyAlertBtn',
|
||||
icon: <EuiIcon type={anomalyAlert ? 'bellSlash' : 'bell'} size="m" />,
|
||||
onClick: () => {
|
||||
if (anomalyAlert) {
|
||||
setIsConfirmAlertDeleteOpen(true);
|
||||
} else {
|
||||
dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY));
|
||||
dispatch(setAlertFlyoutVisible(true));
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.DISABLE_ANOMALY_DETECTION,
|
||||
'data-test-subj': 'uptimeDeleteMLJobBtn',
|
||||
|
@ -82,12 +111,29 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
|
|||
];
|
||||
|
||||
return (
|
||||
<EuiPopover button={button} isOpen={isPopOverOpen} closePopover={() => setIsPopOverOpen(false)}>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={panels}
|
||||
data-test-subj="uptimeManageMLContextMenu"
|
||||
/>
|
||||
</EuiPopover>
|
||||
<>
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopOverOpen}
|
||||
closePopover={() => setIsPopOverOpen(false)}
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={panels}
|
||||
data-test-subj="uptimeManageMLContextMenu"
|
||||
/>
|
||||
</EuiPopover>
|
||||
{isConfirmAlertDeleteOpen && (
|
||||
<ConfirmAlertDeletion
|
||||
onConfirm={() => {
|
||||
deleteAnomalyAlert();
|
||||
setIsConfirmAlertDeleteOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsConfirmAlertDeleteOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,59 +13,61 @@ import {
|
|||
isMLJobCreatingSelector,
|
||||
selectDynamicSettings,
|
||||
} from '../../../state/selectors';
|
||||
import { createMLJobAction, getExistingMLJobAction } from '../../../state/actions';
|
||||
import {
|
||||
createMLJobAction,
|
||||
getExistingMLJobAction,
|
||||
setAlertFlyoutType,
|
||||
setAlertFlyoutVisible,
|
||||
} from '../../../state/actions';
|
||||
import { MLJobLink } from './ml_job_link';
|
||||
import * as labels from './translations';
|
||||
import {
|
||||
useKibana,
|
||||
KibanaReactNotifications,
|
||||
} from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { MLFlyoutView } from './ml_flyout';
|
||||
import { ML_JOB_ID } from '../../../../common/constants';
|
||||
import { CLIENT_ALERT_TYPES, ML_JOB_ID } from '../../../../common/constants';
|
||||
import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts';
|
||||
import { useGetUrlParams } from '../../../hooks';
|
||||
import { getDynamicSettings } from '../../../state/actions/dynamic_settings';
|
||||
import { useMonitorId } from '../../../hooks';
|
||||
import { kibanaService } from '../../../state/kibana_service';
|
||||
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const showMLJobNotification = (
|
||||
notifications: KibanaReactNotifications,
|
||||
monitorId: string,
|
||||
basePath: string,
|
||||
range: { to: string; from: string },
|
||||
success: boolean,
|
||||
message = ''
|
||||
error?: Error
|
||||
) => {
|
||||
if (success) {
|
||||
notifications.toasts.success({
|
||||
title: (
|
||||
<p data-test-subj="uptimeMLJobSuccessfullyCreated">{labels.JOB_CREATED_SUCCESS_TITLE}</p>
|
||||
),
|
||||
body: (
|
||||
<p>
|
||||
{labels.JOB_CREATED_SUCCESS_MESSAGE}
|
||||
<MLJobLink monitorId={monitorId} basePath={basePath} dateRange={range}>
|
||||
{labels.VIEW_JOB}
|
||||
</MLJobLink>
|
||||
</p>
|
||||
),
|
||||
toastLifeTimeMs: 10000,
|
||||
});
|
||||
kibanaService.toasts.addSuccess(
|
||||
{
|
||||
title: toMountPoint(
|
||||
<p data-test-subj="uptimeMLJobSuccessfullyCreated">{labels.JOB_CREATED_SUCCESS_TITLE}</p>
|
||||
),
|
||||
text: toMountPoint(
|
||||
<p>
|
||||
{labels.JOB_CREATED_SUCCESS_MESSAGE}
|
||||
<MLJobLink monitorId={monitorId} basePath={basePath} dateRange={range}>
|
||||
{labels.VIEW_JOB}
|
||||
</MLJobLink>
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{ toastLifeTimeMs: 10000 }
|
||||
);
|
||||
} else {
|
||||
notifications.toasts.danger({
|
||||
title: <p data-test-subj="uptimeMLJobCreationFailed">{labels.JOB_CREATION_FAILED}</p>,
|
||||
body: message ?? <p>{labels.JOB_CREATION_FAILED_MESSAGE}</p>,
|
||||
kibanaService.toasts.addError(error!, {
|
||||
title: labels.JOB_CREATION_FAILED,
|
||||
toastMessage: labels.JOB_CREATION_FAILED_MESSAGE,
|
||||
toastLifeTimeMs: 10000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
||||
const { notifications } = useKibana();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { data: hasMLJob, error } = useSelector(hasNewMLJobSelector);
|
||||
const isMLJobCreating = useSelector(isMLJobCreatingSelector);
|
||||
|
@ -100,7 +102,6 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
|||
if (isCreatingJob && !isMLJobCreating) {
|
||||
if (hasMLJob) {
|
||||
showMLJobNotification(
|
||||
notifications,
|
||||
monitorId as string,
|
||||
basePath,
|
||||
{ to: dateRangeEnd, from: dateRangeStart },
|
||||
|
@ -112,31 +113,22 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
|||
loadMLJob(ML_JOB_ID);
|
||||
|
||||
refreshApp();
|
||||
dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY));
|
||||
dispatch(setAlertFlyoutVisible(true));
|
||||
} else {
|
||||
showMLJobNotification(
|
||||
notifications,
|
||||
monitorId as string,
|
||||
basePath,
|
||||
{ to: dateRangeEnd, from: dateRangeStart },
|
||||
false,
|
||||
error?.message || error?.body?.message
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
setIsCreatingJob(false);
|
||||
onClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
hasMLJob,
|
||||
notifications,
|
||||
onClose,
|
||||
isCreatingJob,
|
||||
error,
|
||||
isMLJobCreating,
|
||||
monitorId,
|
||||
dispatch,
|
||||
basePath,
|
||||
]);
|
||||
}, [hasMLJob, onClose, isCreatingJob, error, isMLJobCreating, monitorId, dispatch, basePath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExistingMLJob && !isMLJobCreating && !hasMLJob && heartbeatIndices) {
|
||||
|
|
|
@ -16,12 +16,12 @@ import {
|
|||
import { deleteMLJobAction, getExistingMLJobAction, resetMLState } from '../../../state/actions';
|
||||
import { ConfirmJobDeletion } from './confirm_delete';
|
||||
import { UptimeRefreshContext } from '../../../contexts';
|
||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
||||
import * as labels from './translations';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ManageMLJobComponent } from './manage_ml_job';
|
||||
import { JobStat } from '../../../../../../plugins/ml/public';
|
||||
import { useMonitorId } from '../../../hooks';
|
||||
import { getMLJobId } from '../../../../common/lib';
|
||||
|
||||
export const MLIntegrationComponent = () => {
|
||||
const [isMlFlyoutOpen, setIsMlFlyoutOpen] = useState(false);
|
||||
|
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
|||
import url from 'url';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
||||
import { getMLJobId } from '../../../../common/lib';
|
||||
|
||||
interface Props {
|
||||
monitorId: string;
|
||||
|
|
|
@ -89,6 +89,20 @@ export const DISABLE_ANOMALY_DETECTION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ENABLE_ANOMALY_ALERT = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyAlert',
|
||||
{
|
||||
defaultMessage: 'Enable anomaly alert',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISABLE_ANOMALY_ALERT = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyAlert',
|
||||
{
|
||||
defaultMessage: 'Disable anomaly alert',
|
||||
}
|
||||
);
|
||||
|
||||
export const MANAGE_ANOMALY_DETECTION = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.manageAnomalyDetectionTitle',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getExistingAlertAction } from '../../../state/actions/alerts';
|
||||
import { alertSelector, selectAlertFlyoutVisibility } from '../../../state/selectors';
|
||||
import { UptimeRefreshContext } from '../../../contexts';
|
||||
import { useMonitorId } from '../../../hooks';
|
||||
|
||||
export const useAnomalyAlert = () => {
|
||||
const { lastRefresh } = useContext(UptimeRefreshContext);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const monitorId = useMonitorId();
|
||||
|
||||
const { data: anomalyAlert } = useSelector(alertSelector);
|
||||
|
||||
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getExistingAlertAction.get({ monitorId }));
|
||||
}, [monitorId, lastRefresh, dispatch, alertFlyoutVisible]);
|
||||
|
||||
return anomalyAlert;
|
||||
};
|
|
@ -19,10 +19,10 @@ import {
|
|||
selectDurationLines,
|
||||
} from '../../../state/selectors';
|
||||
import { UptimeRefreshContext } from '../../../contexts';
|
||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
||||
import { JobStat } from '../../../../../ml/public';
|
||||
import { MonitorDurationComponent } from './monitor_duration';
|
||||
import { MonitorIdParam } from '../../../../common/types';
|
||||
import { getMLJobId } from '../../../../common/lib';
|
||||
|
||||
export const MonitorDuration: React.FC<MonitorIdParam> = ({ monitorId }) => {
|
||||
const {
|
||||
|
|
|
@ -14,8 +14,8 @@ interface AlertExpressionPopoverProps {
|
|||
'data-test-subj': string;
|
||||
isEnabled?: boolean;
|
||||
id: string;
|
||||
value: string | JSX.Element;
|
||||
isInvalid?: boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const getColor = (
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiExpression,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiSpacer,
|
||||
EuiHealth,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AnomalyTranslations } from './translations';
|
||||
import { AlertExpressionPopover } from '../alert_expression_popover';
|
||||
import { DEFAULT_SEVERITY, SelectSeverity } from './select_severity';
|
||||
import { monitorIdSelector } from '../../../../state/selectors';
|
||||
import { getSeverityColor, getSeverityType } from '../../../../../../ml/public';
|
||||
|
||||
interface Props {
|
||||
alertParams: { [key: string]: any };
|
||||
setAlertParams: (key: string, value: any) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function AnomalyAlertComponent({ setAlertParams, alertParams }: Props) {
|
||||
const [severity, setSeverity] = useState(DEFAULT_SEVERITY);
|
||||
|
||||
const monitorIdStore = useSelector(monitorIdSelector);
|
||||
|
||||
const monitorId = monitorIdStore || alertParams?.monitorId;
|
||||
|
||||
useEffect(() => {
|
||||
setAlertParams('monitorId', monitorId);
|
||||
}, [monitorId, setAlertParams]);
|
||||
|
||||
useEffect(() => {
|
||||
setAlertParams('severity', severity.val);
|
||||
}, [severity, setAlertParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup direction={'column'} gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiExpression
|
||||
description={AnomalyTranslations.whenMonitor}
|
||||
value={
|
||||
<EuiText className="eui-displayInlineBlock">
|
||||
<h5>{monitorId}</h5>
|
||||
</EuiText>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertExpressionPopover
|
||||
aria-label={AnomalyTranslations.scoreAriaLabel}
|
||||
content={
|
||||
<SelectSeverity
|
||||
data-test-subj="uptimeAnomalySeverityValue"
|
||||
value={severity}
|
||||
onChange={setSeverity}
|
||||
/>
|
||||
}
|
||||
data-test-subj={'uptimeAnomalySeverity'}
|
||||
description={AnomalyTranslations.hasAnomalyWithSeverity}
|
||||
id="severity"
|
||||
value={
|
||||
<EuiHealth
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
color={getSeverityColor(severity.val)}
|
||||
>
|
||||
{getSeverityType(severity.val)}
|
||||
</EuiHealth>
|
||||
}
|
||||
isEnabled={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 React, { Fragment, FC, useState, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';
|
||||
import { getSeverityColor } from '../../../../../../ml/public';
|
||||
|
||||
const warningLabel = i18n.translate('xpack.uptime.controls.selectSeverity.warningLabel', {
|
||||
defaultMessage: 'warning',
|
||||
});
|
||||
const minorLabel = i18n.translate('xpack.uptime.controls.selectSeverity.minorLabel', {
|
||||
defaultMessage: 'minor',
|
||||
});
|
||||
const majorLabel = i18n.translate('xpack.uptime.controls.selectSeverity.majorLabel', {
|
||||
defaultMessage: 'major',
|
||||
});
|
||||
const criticalLabel = i18n.translate('xpack.uptime.controls.selectSeverity.criticalLabel', {
|
||||
defaultMessage: 'critical',
|
||||
});
|
||||
|
||||
const optionsMap = {
|
||||
[warningLabel]: 0,
|
||||
[minorLabel]: 25,
|
||||
[majorLabel]: 50,
|
||||
[criticalLabel]: 75,
|
||||
};
|
||||
|
||||
interface TableSeverity {
|
||||
val: number;
|
||||
display: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const SEVERITY_OPTIONS: TableSeverity[] = [
|
||||
{
|
||||
val: 0,
|
||||
display: warningLabel,
|
||||
color: getSeverityColor(0),
|
||||
},
|
||||
{
|
||||
val: 25,
|
||||
display: minorLabel,
|
||||
color: getSeverityColor(25),
|
||||
},
|
||||
{
|
||||
val: 50,
|
||||
display: majorLabel,
|
||||
color: getSeverityColor(50),
|
||||
},
|
||||
{
|
||||
val: 75,
|
||||
display: criticalLabel,
|
||||
color: getSeverityColor(75),
|
||||
},
|
||||
];
|
||||
|
||||
function optionValueToThreshold(value: number) {
|
||||
// Get corresponding threshold object with required display and val properties from the specified value.
|
||||
let threshold = SEVERITY_OPTIONS.find((opt) => opt.val === value);
|
||||
|
||||
// Default to warning if supplied value doesn't map to one of the options.
|
||||
if (threshold === undefined) {
|
||||
threshold = SEVERITY_OPTIONS[0];
|
||||
}
|
||||
|
||||
return threshold;
|
||||
}
|
||||
|
||||
export const DEFAULT_SEVERITY = SEVERITY_OPTIONS[3];
|
||||
|
||||
const getSeverityOptions = () =>
|
||||
SEVERITY_OPTIONS.map(({ color, display, val }) => ({
|
||||
'data-test-subj': `alertAnomaly${display}`,
|
||||
value: display,
|
||||
inputDisplay: (
|
||||
<Fragment>
|
||||
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
|
||||
{display}
|
||||
</EuiHealth>
|
||||
</Fragment>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<Fragment>
|
||||
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
|
||||
{display}
|
||||
</EuiHealth>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p className="euiTextColor--subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.controls.selectSeverity.scoreDetailsDescription"
|
||||
defaultMessage="score {value} and above"
|
||||
values={{ value: val }}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
),
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
onChange: (sev: TableSeverity) => void;
|
||||
value: TableSeverity;
|
||||
}
|
||||
|
||||
export const SelectSeverity: FC<Props> = ({ onChange, value }) => {
|
||||
const [severity, setSeverity] = useState(DEFAULT_SEVERITY);
|
||||
|
||||
const onSeverityChange = (valueDisplay: string) => {
|
||||
const option = optionValueToThreshold(optionsMap[valueDisplay]);
|
||||
setSeverity(option);
|
||||
onChange(option);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSeverity(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
hasDividers
|
||||
style={{ width: 200 }}
|
||||
options={getSeverityOptions()}
|
||||
valueOfSelected={severity.display}
|
||||
onChange={onSeverityChange}
|
||||
data-test-subj={'anomalySeveritySelect'}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AnomalyTranslations = {
|
||||
criteriaAriaLabel: i18n.translate('xpack.uptime.alerts.anomaly.criteriaExpression.ariaLabel', {
|
||||
defaultMessage: 'An expression displaying the criteria for a selected monitor.',
|
||||
}),
|
||||
whenMonitor: i18n.translate('xpack.uptime.alerts.anomaly.criteriaExpression.description', {
|
||||
defaultMessage: 'When monitor',
|
||||
}),
|
||||
scoreAriaLabel: i18n.translate('xpack.uptime.alerts.anomaly.scoreExpression.ariaLabel', {
|
||||
defaultMessage: 'An expression displaying the criteria for an anomaly alert threshold.',
|
||||
}),
|
||||
hasAnomalyWithSeverity: i18n.translate(
|
||||
'xpack.uptime.alerts.anomaly.scoreExpression.description',
|
||||
{
|
||||
defaultMessage: 'has anomaly with severity',
|
||||
description: 'An expression displaying the criteria for an anomaly alert threshold.',
|
||||
}
|
||||
),
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
|
||||
import { DurationAnomalyTranslations } from './translations';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { store } from '../../state';
|
||||
|
||||
const { name, defaultActionMessage } = DurationAnomalyTranslations;
|
||||
const AnomalyAlertExpression = React.lazy(() =>
|
||||
import('../../components/overview/alerts/anomaly_alert/anomaly_alert')
|
||||
);
|
||||
export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
|
||||
core,
|
||||
plugins,
|
||||
}): AlertTypeModel => ({
|
||||
id: CLIENT_ALERT_TYPES.DURATION_ANOMALY,
|
||||
iconClass: 'uptimeApp',
|
||||
alertParamsExpression: (params: any) => (
|
||||
<ReduxProvider store={store}>
|
||||
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||
<AnomalyAlertExpression {...params} />
|
||||
</KibanaContextProvider>
|
||||
</ReduxProvider>
|
||||
),
|
||||
name,
|
||||
validate: () => ({ errors: {} }),
|
||||
defaultActionMessage,
|
||||
requiresAppContext: false,
|
||||
});
|
|
@ -9,6 +9,7 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
|||
import { initMonitorStatusAlertType } from './monitor_status';
|
||||
import { initTlsAlertType } from './tls';
|
||||
import { ClientPluginsStart } from '../../apps/plugin';
|
||||
import { initDurationAnomalyAlertType } from './duration_anomaly';
|
||||
|
||||
export type AlertTypeInitializer = (dependenies: {
|
||||
core: CoreStart;
|
||||
|
@ -18,4 +19,5 @@ export type AlertTypeInitializer = (dependenies: {
|
|||
export const alertTypeInitializers: AlertTypeInitializer[] = [
|
||||
initMonitorStatusAlertType,
|
||||
initTlsAlertType,
|
||||
initDurationAnomalyAlertType,
|
||||
];
|
||||
|
|
|
@ -26,7 +26,7 @@ export const TlsTranslations = {
|
|||
{expiringConditionalOpen}
|
||||
Expiring cert count: {expiringCount}
|
||||
Expiring Certificates: {expiringCommonNameAndDate}
|
||||
{expiringConditionalClose}
|
||||
{expiringConditionalClose}
|
||||
|
||||
{agingConditionalOpen}
|
||||
Aging cert count: {agingCount}
|
||||
|
@ -49,3 +49,23 @@ Aging Certificates: {agingCommonNameAndDate}
|
|||
defaultMessage: 'Uptime TLS',
|
||||
}),
|
||||
};
|
||||
|
||||
export const DurationAnomalyTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
|
||||
defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
|
||||
Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
|
||||
values: {
|
||||
severity: '{{state.severity}}',
|
||||
anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
|
||||
monitor: '{{state.monitor}}',
|
||||
monitorUrl: '{{{state.monitorUrl}}}',
|
||||
slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
|
||||
expectedResponseTime: '{{state.expectedResponseTime}}',
|
||||
severityScore: '{{state.severityScore}}',
|
||||
observerLocation: '{{state.observerLocation}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
|
||||
defaultMessage: 'Uptime Duration Anomaly',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import { MonitorCharts } from '../components/monitor';
|
|||
import { MonitorStatusDetails, PingList } from '../components/monitor';
|
||||
import { getDynamicSettings } from '../state/actions/dynamic_settings';
|
||||
import { Ping } from '../../common/runtime_types/ping';
|
||||
import { setSelectedMonitorId } from '../state/actions';
|
||||
|
||||
const isAutogeneratedId = (id: string) => {
|
||||
const autoGeneratedId = /^auto-(icmp|http|tcp)-OX[A-F0-9]{16}-[a-f0-9]{16}/;
|
||||
|
@ -43,6 +44,10 @@ export const MonitorPage: React.FC = () => {
|
|||
|
||||
const monitorId = useMonitorId();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setSelectedMonitorId(monitorId));
|
||||
}, [monitorId, dispatch]);
|
||||
|
||||
const selectedMonitor = useSelector(monitorStatusSelector);
|
||||
|
||||
useTrackPageview({ app: 'uptime', path: 'monitor' });
|
||||
|
|
15
x-pack/plugins/uptime/public/state/actions/alerts.ts
Normal file
15
x-pack/plugins/uptime/public/state/actions/alerts.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { createAsyncAction } from './utils';
|
||||
import { MonitorIdParam } from './types';
|
||||
import { Alert } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
export const getExistingAlertAction = createAsyncAction<MonitorIdParam, Alert>(
|
||||
'GET EXISTING ALERTS'
|
||||
);
|
||||
|
||||
export const deleteAlertAction = createAsyncAction<{ alertId: string }, any>('DELETE ALERTS');
|
|
@ -25,3 +25,5 @@ export const setSearchTextAction = createAction<string>('SET SEARCH');
|
|||
export const toggleIntegrationsPopover = createAction<PopoverState>(
|
||||
'TOGGLE INTEGRATION POPOVER STATE'
|
||||
);
|
||||
|
||||
export const setSelectedMonitorId = createAction<string>('SET MONITOR ID');
|
||||
|
|
27
x-pack/plugins/uptime/public/state/api/alerts.ts
Normal file
27
x-pack/plugins/uptime/public/state/api/alerts.ts
Normal 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 { apiService } from './utils';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { MonitorIdParam } from '../actions/types';
|
||||
import { Alert } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
export const fetchAlertRecords = async ({ monitorId }: MonitorIdParam): Promise<Alert> => {
|
||||
const data = {
|
||||
page: 1,
|
||||
per_page: 500,
|
||||
filter: 'alert.attributes.alertTypeId:(xpack.uptime.alerts.durationAnomaly)',
|
||||
default_search_operator: 'AND',
|
||||
sort_field: 'name.keyword',
|
||||
sort_order: 'asc',
|
||||
};
|
||||
const alerts = await apiService.get(API_URLS.ALERTS_FIND, data);
|
||||
return alerts.data.find((alert: Alert) => alert.params.monitorId === monitorId);
|
||||
};
|
||||
|
||||
export const disableAnomalyAlert = async ({ alertId }: { alertId: string }) => {
|
||||
return await apiService.delete(API_URLS.ALERT + alertId);
|
||||
};
|
|
@ -7,38 +7,19 @@
|
|||
import moment from 'moment';
|
||||
import { apiService } from './utils';
|
||||
import { AnomalyRecords, AnomalyRecordsParams } from '../actions';
|
||||
import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants';
|
||||
import { API_URLS, ML_MODULE_ID } from '../../../common/constants';
|
||||
import {
|
||||
MlCapabilitiesResponse,
|
||||
DataRecognizerConfigResponse,
|
||||
JobExistResult,
|
||||
MlCapabilitiesResponse,
|
||||
} from '../../../../../plugins/ml/public';
|
||||
import {
|
||||
CreateMLJobSuccess,
|
||||
DeleteJobResults,
|
||||
MonitorIdParam,
|
||||
HeartbeatIndicesParam,
|
||||
MonitorIdParam,
|
||||
} from '../actions/types';
|
||||
|
||||
const getJobPrefix = (monitorId: string) => {
|
||||
// ML App doesn't support upper case characters in job name
|
||||
// Also Spaces and the characters / ? , " < > | * are not allowed
|
||||
// so we will replace all special chars with _
|
||||
|
||||
const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase();
|
||||
|
||||
// ML Job ID can't be greater than 64 length, so will be substring it, and hope
|
||||
// At such big length, there is minimum chance of having duplicate monitor id
|
||||
// Subtracting ML_JOB_ID constant as well
|
||||
const postfix = '_' + ML_JOB_ID;
|
||||
|
||||
if ((prefix + postfix).length > 64) {
|
||||
return prefix.substring(0, 64 - postfix.length) + '_';
|
||||
}
|
||||
return prefix + '_';
|
||||
};
|
||||
|
||||
export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`;
|
||||
import { getJobPrefix, getMLJobId } from '../../../common/lib/ml';
|
||||
|
||||
export const getMLCapabilities = async (): Promise<MlCapabilitiesResponse> => {
|
||||
return await apiService.get(API_URLS.ML_CAPABILITIES);
|
||||
|
|
39
x-pack/plugins/uptime/public/state/effects/alerts.ts
Normal file
39
x-pack/plugins/uptime/public/state/effects/alerts.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Action } from 'redux-actions';
|
||||
import { call, put, takeLatest, select } from 'redux-saga/effects';
|
||||
import { fetchEffectFactory } from './fetch_effect';
|
||||
import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts';
|
||||
import { disableAnomalyAlert, fetchAlertRecords } from '../api/alerts';
|
||||
import { kibanaService } from '../kibana_service';
|
||||
import { monitorIdSelector } from '../selectors';
|
||||
|
||||
export function* fetchAlertsEffect() {
|
||||
yield takeLatest(
|
||||
getExistingAlertAction.get,
|
||||
fetchEffectFactory(
|
||||
fetchAlertRecords,
|
||||
getExistingAlertAction.success,
|
||||
getExistingAlertAction.fail
|
||||
)
|
||||
);
|
||||
|
||||
yield takeLatest(String(deleteAlertAction.get), function* (action: Action<{ alertId: string }>) {
|
||||
try {
|
||||
const response = yield call(disableAnomalyAlert, action.payload);
|
||||
yield put(deleteAlertAction.success(response));
|
||||
kibanaService.core.notifications.toasts.addSuccess('Alert successfully deleted!');
|
||||
const monitorId = yield select(monitorIdSelector);
|
||||
yield put(getExistingAlertAction.get({ monitorId }));
|
||||
} catch (err) {
|
||||
kibanaService.core.notifications.toasts.addError(err, {
|
||||
title: 'Alert cannot be deleted',
|
||||
});
|
||||
yield put(deleteAlertAction.fail(err));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -17,6 +17,7 @@ import { fetchMonitorDurationEffect } from './monitor_duration';
|
|||
import { fetchMLJobEffect } from './ml_anomaly';
|
||||
import { fetchIndexStatusEffect } from './index_status';
|
||||
import { fetchCertificatesEffect } from '../certificates/certificates';
|
||||
import { fetchAlertsEffect } from './alerts';
|
||||
|
||||
export function* rootEffect() {
|
||||
yield fork(fetchMonitorDetailsEffect);
|
||||
|
@ -33,4 +34,5 @@ export function* rootEffect() {
|
|||
yield fork(fetchMonitorDurationEffect);
|
||||
yield fork(fetchIndexStatusEffect);
|
||||
yield fork(fetchCertificatesEffect);
|
||||
yield fork(fetchAlertsEffect);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import { Action } from 'redux-actions';
|
||||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import {
|
||||
getMLCapabilitiesAction,
|
||||
getExistingMLJobAction,
|
||||
|
@ -20,6 +21,9 @@ import {
|
|||
deleteMLJob,
|
||||
getMLCapabilities,
|
||||
} from '../api/ml_anomaly';
|
||||
import { deleteAlertAction } from '../actions/alerts';
|
||||
import { alertSelector } from '../selectors';
|
||||
import { MonitorIdParam } from '../actions/types';
|
||||
|
||||
export function* fetchMLJobEffect() {
|
||||
yield takeLatest(
|
||||
|
@ -38,10 +42,22 @@ export function* fetchMLJobEffect() {
|
|||
getAnomalyRecordsAction.fail
|
||||
)
|
||||
);
|
||||
yield takeLatest(
|
||||
deleteMLJobAction.get,
|
||||
fetchEffectFactory(deleteMLJob, deleteMLJobAction.success, deleteMLJobAction.fail)
|
||||
);
|
||||
|
||||
yield takeLatest(String(deleteMLJobAction.get), function* (action: Action<MonitorIdParam>) {
|
||||
try {
|
||||
const response = yield call(deleteMLJob, action.payload);
|
||||
yield put(deleteMLJobAction.success(response));
|
||||
|
||||
// let's delete alert as well if it's there
|
||||
const { data: anomalyAlert } = yield select(alertSelector);
|
||||
if (anomalyAlert) {
|
||||
yield put(deleteAlertAction.get({ alertId: anomalyAlert.id as string }));
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(deleteMLJobAction.fail(err));
|
||||
}
|
||||
});
|
||||
|
||||
yield takeLatest(
|
||||
getMLCapabilitiesAction.get,
|
||||
fetchEffectFactory(
|
||||
|
|
|
@ -20,6 +20,10 @@ class KibanaService {
|
|||
apiService.http = this._core.http;
|
||||
}
|
||||
|
||||
public get toasts() {
|
||||
return this._core.notifications.toasts;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): KibanaService {
|
||||
|
|
|
@ -9,6 +9,7 @@ Object {
|
|||
"id": "popover-2",
|
||||
"open": true,
|
||||
},
|
||||
"monitorId": "test",
|
||||
"searchText": "",
|
||||
}
|
||||
`;
|
||||
|
@ -19,6 +20,7 @@ Object {
|
|||
"basePath": "yyz",
|
||||
"esKuery": "",
|
||||
"integrationsPopoverOpen": null,
|
||||
"monitorId": "test",
|
||||
"searchText": "",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -24,6 +24,7 @@ describe('ui reducer', () => {
|
|||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
searchText: '',
|
||||
monitorId: 'test',
|
||||
},
|
||||
action
|
||||
)
|
||||
|
@ -43,6 +44,7 @@ describe('ui reducer', () => {
|
|||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
searchText: '',
|
||||
monitorId: 'test',
|
||||
},
|
||||
action
|
||||
)
|
||||
|
@ -59,6 +61,7 @@ describe('ui reducer', () => {
|
|||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
searchText: '',
|
||||
monitorId: 'test',
|
||||
},
|
||||
action
|
||||
)
|
||||
|
@ -68,6 +71,7 @@ describe('ui reducer', () => {
|
|||
"basePath": "",
|
||||
"esKuery": "",
|
||||
"integrationsPopoverOpen": null,
|
||||
"monitorId": "test",
|
||||
"searchText": "",
|
||||
}
|
||||
`);
|
||||
|
@ -83,6 +87,7 @@ describe('ui reducer', () => {
|
|||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
searchText: '',
|
||||
monitorId: 'test',
|
||||
},
|
||||
action
|
||||
)
|
||||
|
@ -92,6 +97,7 @@ describe('ui reducer', () => {
|
|||
"basePath": "",
|
||||
"esKuery": "",
|
||||
"integrationsPopoverOpen": null,
|
||||
"monitorId": "test",
|
||||
"searchText": "lorem ipsum",
|
||||
}
|
||||
`);
|
||||
|
|
29
x-pack/plugins/uptime/public/state/reducers/alerts.ts
Normal file
29
x-pack/plugins/uptime/public/state/reducers/alerts.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { handleActions } from 'redux-actions';
|
||||
import { getAsyncInitialState, handleAsyncAction } from './utils';
|
||||
import { AsyncInitialState } from './types';
|
||||
import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts';
|
||||
import { Alert } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
export interface AlertsState {
|
||||
alert: AsyncInitialState<Alert>;
|
||||
alertDeletion: AsyncInitialState<boolean>;
|
||||
}
|
||||
|
||||
const initialState: AlertsState = {
|
||||
alert: getAsyncInitialState(),
|
||||
alertDeletion: getAsyncInitialState(),
|
||||
};
|
||||
|
||||
export const alertsReducer = handleActions<AlertsState>(
|
||||
{
|
||||
...handleAsyncAction<AlertsState>('alert', getExistingAlertAction),
|
||||
...handleAsyncAction<AlertsState>('alertDeletion', deleteAlertAction),
|
||||
},
|
||||
initialState
|
||||
);
|
|
@ -20,6 +20,7 @@ import { indexStatusReducer } from './index_status';
|
|||
import { mlJobsReducer } from './ml_anomaly';
|
||||
import { certificatesReducer } from '../certificates/certificates';
|
||||
import { selectedFiltersReducer } from './selected_filters';
|
||||
import { alertsReducer } from './alerts';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
monitor: monitorReducer,
|
||||
|
@ -37,4 +38,5 @@ export const rootReducer = combineReducers({
|
|||
indexStatus: indexStatusReducer,
|
||||
certificates: certificatesReducer,
|
||||
selectedFilters: selectedFiltersReducer,
|
||||
alerts: alertsReducer,
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
setAlertFlyoutType,
|
||||
setAlertFlyoutVisible,
|
||||
setSearchTextAction,
|
||||
setSelectedMonitorId,
|
||||
} from '../actions';
|
||||
|
||||
export interface UiState {
|
||||
|
@ -23,6 +24,7 @@ export interface UiState {
|
|||
esKuery: string;
|
||||
searchText: string;
|
||||
integrationsPopoverOpen: PopoverState | null;
|
||||
monitorId: string;
|
||||
}
|
||||
|
||||
const initialState: UiState = {
|
||||
|
@ -31,6 +33,7 @@ const initialState: UiState = {
|
|||
esKuery: '',
|
||||
searchText: '',
|
||||
integrationsPopoverOpen: null,
|
||||
monitorId: '',
|
||||
};
|
||||
|
||||
export const uiReducer = handleActions<UiState, UiPayload>(
|
||||
|
@ -64,6 +67,10 @@ export const uiReducer = handleActions<UiState, UiPayload>(
|
|||
...state,
|
||||
searchText: action.payload,
|
||||
}),
|
||||
[String(setSelectedMonitorId)]: (state, action: Action<string>) => ({
|
||||
...state,
|
||||
monitorId: action.payload,
|
||||
}),
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('state selectors', () => {
|
|||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
searchText: '',
|
||||
monitorId: '',
|
||||
},
|
||||
monitorStatus: {
|
||||
status: null,
|
||||
|
@ -108,6 +109,10 @@ describe('state selectors', () => {
|
|||
},
|
||||
},
|
||||
selectedFilters: null,
|
||||
alerts: {
|
||||
alertDeletion: { data: null, loading: false },
|
||||
alert: { data: null, loading: false },
|
||||
},
|
||||
};
|
||||
|
||||
it('selects base path from state', () => {
|
||||
|
|
|
@ -59,6 +59,8 @@ export const hasNewMLJobSelector = ({ ml }: AppState) => ml.createJob;
|
|||
export const isMLJobCreatingSelector = ({ ml }: AppState) => ml.createJob.loading;
|
||||
|
||||
export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading;
|
||||
export const isAnomalyAlertDeletingSelector = ({ alerts }: AppState) =>
|
||||
alerts.alertDeletion.loading;
|
||||
|
||||
export const isMLJobDeletedSelector = ({ ml }: AppState) => ml.deleteJob;
|
||||
|
||||
|
@ -88,3 +90,7 @@ export const esKuerySelector = ({ ui: { esKuery } }: AppState) => esKuery;
|
|||
export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchText;
|
||||
|
||||
export const selectedFiltersSelector = ({ selectedFilters }: AppState) => selectedFilters;
|
||||
|
||||
export const monitorIdSelector = ({ ui: { monitorId } }: AppState) => monitorId;
|
||||
|
||||
export const alertSelector = ({ alerts }: AppState) => alerts.alert;
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { UMKibanaRoute } from '../../../rest_api';
|
||||
import { PluginSetupContract } from '../../../../../features/server';
|
||||
import { DynamicSettings } from '../../../../common/runtime_types';
|
||||
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
|
||||
|
||||
export type APICaller = (
|
||||
endpoint: string,
|
||||
|
@ -39,6 +40,7 @@ export interface UptimeCorePlugins {
|
|||
alerts: any;
|
||||
elasticsearch: any;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
ml: MlSetup;
|
||||
}
|
||||
|
||||
export interface UMBackendFrameworkAdapter {
|
||||
|
|
|
@ -17,7 +17,7 @@ import { GetMonitorStatusResult } from '../../requests';
|
|||
import { AlertType } from '../../../../../alerts/server';
|
||||
import { IRouter } from 'kibana/server';
|
||||
import { UMServerLibs } from '../../lib';
|
||||
import { UptimeCoreSetup } from '../../adapters';
|
||||
import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
|
||||
import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks';
|
||||
|
||||
|
@ -33,9 +33,10 @@ const bootstrapDependencies = (customRequests?: any) => {
|
|||
// these server/libs parameters don't have any functionality, which is fine
|
||||
// because we aren't testing them here
|
||||
const server: UptimeCoreSetup = { router };
|
||||
const plugins: UptimeCorePlugins = {} as any;
|
||||
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
|
||||
libs.requests = { ...libs.requests, ...customRequests };
|
||||
return { server, libs };
|
||||
return { server, libs, plugins };
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -82,8 +83,8 @@ describe('status check alert', () => {
|
|||
expect.assertions(4);
|
||||
const mockGetter = jest.fn();
|
||||
mockGetter.mockReturnValue([]);
|
||||
const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
// @ts-ignore the executor can return `void`, but ours never does
|
||||
const state: Record<string, any> = await alert.executor(mockOptions());
|
||||
|
||||
|
@ -128,8 +129,8 @@ describe('status check alert', () => {
|
|||
status: 'down',
|
||||
},
|
||||
]);
|
||||
const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions();
|
||||
const alertServices: AlertServicesMock = options.services;
|
||||
// @ts-ignore the executor can return `void`, but ours never does
|
||||
|
@ -213,11 +214,11 @@ describe('status check alert', () => {
|
|||
status: 'down',
|
||||
},
|
||||
]);
|
||||
const { server, libs } = bootstrapDependencies({
|
||||
const { server, libs, plugins } = bootstrapDependencies({
|
||||
getMonitorStatus: mockGetter,
|
||||
getIndexPattern: jest.fn(),
|
||||
});
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions({
|
||||
numTimes: 4,
|
||||
timerange: { from: 'now-14h', to: 'now' },
|
||||
|
@ -286,11 +287,11 @@ describe('status check alert', () => {
|
|||
status: 'down',
|
||||
},
|
||||
]);
|
||||
const { server, libs } = bootstrapDependencies({
|
||||
const { server, libs, plugins } = bootstrapDependencies({
|
||||
getMonitorStatus: mockGetter,
|
||||
getIndexPattern: jest.fn(),
|
||||
});
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions({
|
||||
numTimes: 3,
|
||||
timerangeUnit: 'm',
|
||||
|
@ -371,11 +372,11 @@ describe('status check alert', () => {
|
|||
toISOStringSpy.mockImplementation(() => 'search test');
|
||||
const mockGetter = jest.fn();
|
||||
mockGetter.mockReturnValue([]);
|
||||
const { server, libs } = bootstrapDependencies({
|
||||
const { server, libs, plugins } = bootstrapDependencies({
|
||||
getIndexPattern: jest.fn(),
|
||||
getMonitorStatus: mockGetter,
|
||||
});
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions({
|
||||
numTimes: 20,
|
||||
timerangeCount: 30,
|
||||
|
@ -467,12 +468,12 @@ describe('status check alert', () => {
|
|||
availabilityRatio: 0.909245845760545,
|
||||
},
|
||||
]);
|
||||
const { server, libs } = bootstrapDependencies({
|
||||
const { server, libs, plugins } = bootstrapDependencies({
|
||||
getMonitorAvailability: mockAvailability,
|
||||
getMonitorStatus: mockGetter,
|
||||
getIndexPattern: jest.fn(),
|
||||
});
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions({
|
||||
availability: {
|
||||
range: 35,
|
||||
|
@ -559,11 +560,11 @@ describe('status check alert', () => {
|
|||
mockGetter.mockReturnValue([]);
|
||||
const mockAvailability = jest.fn();
|
||||
mockAvailability.mockReturnValue([]);
|
||||
const { server, libs } = bootstrapDependencies({
|
||||
const { server, libs, plugins } = bootstrapDependencies({
|
||||
getMonitorAvailability: mockAvailability,
|
||||
getIndexPattern: jest.fn(),
|
||||
});
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions({
|
||||
availability: {
|
||||
range: 23,
|
||||
|
@ -600,11 +601,11 @@ describe('status check alert', () => {
|
|||
mockGetter.mockReturnValue([]);
|
||||
const mockAvailability = jest.fn();
|
||||
mockAvailability.mockReturnValue([]);
|
||||
const { server, libs } = bootstrapDependencies({
|
||||
const { server, libs, plugins } = bootstrapDependencies({
|
||||
getMonitorAvailability: mockAvailability,
|
||||
getIndexPattern: jest.fn(),
|
||||
});
|
||||
const alert = statusCheckAlertFactory(server, libs);
|
||||
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions({
|
||||
availability: {
|
||||
range: 23,
|
||||
|
@ -748,8 +749,8 @@ describe('status check alert', () => {
|
|||
let alert: AlertType;
|
||||
|
||||
beforeEach(() => {
|
||||
const { server, libs } = bootstrapDependencies();
|
||||
alert = statusCheckAlertFactory(server, libs);
|
||||
const { server, libs, plugins } = bootstrapDependencies();
|
||||
alert = statusCheckAlertFactory(server, libs, plugins);
|
||||
});
|
||||
|
||||
it('creates an alert with expected params', () => {
|
||||
|
|
129
x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
Normal file
129
x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ILegacyScopedClusterClient } from 'kibana/server';
|
||||
import { updateState } from './common';
|
||||
import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants';
|
||||
import { commonStateTranslations, durationAnomalyTranslations } from './translations';
|
||||
import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies';
|
||||
import { getSeverityType } from '../../../../ml/common/util/anomaly_utils';
|
||||
import { getLatestMonitor } from '../requests';
|
||||
import { savedObjectsAdapter } from '../saved_objects';
|
||||
import { UptimeCorePlugins } from '../adapters/framework';
|
||||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import { getMLJobId } from '../../../common/lib';
|
||||
|
||||
const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS;
|
||||
|
||||
export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Ping) => {
|
||||
return {
|
||||
severity: getSeverityType(anomaly.severity),
|
||||
severityScore: Math.round(anomaly.severity),
|
||||
anomalyStartTimestamp: moment(anomaly.source.timestamp).toISOString(),
|
||||
monitor: anomaly.source['monitor.id'],
|
||||
monitorUrl: monitorInfo.url?.full,
|
||||
slowestAnomalyResponse: Math.round(anomaly.actualSort / 1000) + ' ms',
|
||||
expectedResponseTime: Math.round(anomaly.typicalSort / 1000) + ' ms',
|
||||
observerLocation: anomaly.entityValue,
|
||||
};
|
||||
};
|
||||
|
||||
const getAnomalies = async (
|
||||
plugins: UptimeCorePlugins,
|
||||
mlClusterClient: ILegacyScopedClusterClient,
|
||||
params: Record<any, any>,
|
||||
lastCheckedAt: string
|
||||
) => {
|
||||
const { getAnomaliesTableData } = plugins.ml.resultsServiceProvider(mlClusterClient, {
|
||||
params: 'DummyKibanaRequest',
|
||||
} as any);
|
||||
|
||||
return await getAnomaliesTableData(
|
||||
[getMLJobId(params.monitorId)],
|
||||
[],
|
||||
[],
|
||||
'auto',
|
||||
params.severity,
|
||||
moment(lastCheckedAt).valueOf(),
|
||||
moment().valueOf(),
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
500,
|
||||
10,
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _libs, plugins) => ({
|
||||
id: 'xpack.uptime.alerts.durationAnomaly',
|
||||
name: durationAnomalyTranslations.alertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
monitorId: schema.string(),
|
||||
severity: schema.number(),
|
||||
}),
|
||||
},
|
||||
defaultActionGroupId: DURATION_ANOMALY.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: DURATION_ANOMALY.id,
|
||||
name: DURATION_ANOMALY.name,
|
||||
},
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
producer: 'uptime',
|
||||
async executor(options) {
|
||||
const {
|
||||
services: {
|
||||
alertInstanceFactory,
|
||||
callCluster,
|
||||
savedObjectsClient,
|
||||
getLegacyScopedClusterClient,
|
||||
},
|
||||
state,
|
||||
params,
|
||||
} = options;
|
||||
|
||||
const { anomalies } =
|
||||
(await getAnomalies(
|
||||
plugins,
|
||||
getLegacyScopedClusterClient(plugins.ml.mlClient),
|
||||
params,
|
||||
state.lastCheckedAt
|
||||
)) ?? {};
|
||||
|
||||
const foundAnomalies = anomalies?.length > 0;
|
||||
|
||||
if (foundAnomalies) {
|
||||
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
|
||||
savedObjectsClient
|
||||
);
|
||||
const monitorInfo = await getLatestMonitor({
|
||||
dynamicSettings,
|
||||
callES: callCluster,
|
||||
dateStart: 'now-15m',
|
||||
dateEnd: 'now',
|
||||
monitorId: params.monitorId,
|
||||
});
|
||||
anomalies.forEach((anomaly, index) => {
|
||||
const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index);
|
||||
const summary = getAnomalySummary(anomaly, monitorInfo);
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, false),
|
||||
...summary,
|
||||
});
|
||||
alertInstance.scheduleActions(DURATION_ANOMALY.id);
|
||||
});
|
||||
}
|
||||
|
||||
return updateState(state, foundAnomalies);
|
||||
},
|
||||
});
|
|
@ -7,8 +7,10 @@
|
|||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { statusCheckAlertFactory } from './status_check';
|
||||
import { tlsAlertFactory } from './tls';
|
||||
import { durationAnomalyAlertFactory } from './duration_anomaly';
|
||||
|
||||
export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [
|
||||
statusCheckAlertFactory,
|
||||
tlsAlertFactory,
|
||||
durationAnomalyAlertFactory,
|
||||
];
|
||||
|
|
|
@ -148,3 +148,93 @@ export const tlsTranslations = {
|
|||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const durationAnomalyTranslations = {
|
||||
alertFactoryName: i18n.translate('xpack.uptime.alerts.durationAnomaly', {
|
||||
defaultMessage: 'Uptime Duration Anomaly',
|
||||
}),
|
||||
actionVariables: [
|
||||
{
|
||||
name: 'severity',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.severity',
|
||||
{
|
||||
defaultMessage: 'The severity of the anomaly.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'anomalyStartTimestamp',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.anomalyStartTimestamp',
|
||||
{
|
||||
defaultMessage: 'ISO8601 timestamp of the start of the anomaly.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'monitor',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.monitor',
|
||||
{
|
||||
defaultMessage:
|
||||
'A human friendly rendering of name or ID, preferring name (e.g. My Monitor)',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'monitorId',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.monitorId',
|
||||
{
|
||||
defaultMessage: 'ID of the monitor.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'monitorUrl',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.monitorUrl',
|
||||
{
|
||||
defaultMessage: 'URL of the monitor.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'slowestAnomalyResponse',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.slowestAnomalyResponse',
|
||||
{
|
||||
defaultMessage: 'Slowest response time during anomaly bucket with unit (ms, s) attached.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'expectedResponseTime',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.expectedResponseTime',
|
||||
{
|
||||
defaultMessage: 'Expected response time',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'severityScore',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.severityScore',
|
||||
{
|
||||
defaultMessage: 'Anomaly severity score',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'observerLocation',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.durationAnomaly.actionVariables.state.observerLocation',
|
||||
{
|
||||
defaultMessage: 'Observer location from which heartbeat check is performed.',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
*/
|
||||
|
||||
import { AlertType } from '../../../../alerts/server';
|
||||
import { UptimeCoreSetup } from '../adapters';
|
||||
import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters';
|
||||
import { UMServerLibs } from '../lib';
|
||||
|
||||
export type UptimeAlertTypeFactory = (server: UptimeCoreSetup, libs: UMServerLibs) => AlertType;
|
||||
export type UptimeAlertTypeFactory = (
|
||||
server: UptimeCoreSetup,
|
||||
libs: UMServerLibs,
|
||||
plugins: UptimeCorePlugins
|
||||
) => AlertType;
|
||||
|
|
|
@ -19,6 +19,6 @@ export const initUptimeServer = (
|
|||
);
|
||||
|
||||
uptimeAlertTypeFactories.forEach((alertTypeFactory) =>
|
||||
plugins.alerts.registerType(alertTypeFactory(server, libs))
|
||||
plugins.alerts.registerType(alertTypeFactory(server, libs, plugins))
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,12 +20,18 @@ export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) {
|
|||
},
|
||||
|
||||
async openMLManageMenu() {
|
||||
await this.cancelAlertFlyout();
|
||||
return retry.tryForTime(30000, async () => {
|
||||
await testSubjects.click('uptimeManageMLJobBtn');
|
||||
await testSubjects.existOrFail('uptimeManageMLContextMenu');
|
||||
});
|
||||
},
|
||||
|
||||
async cancelAlertFlyout() {
|
||||
if (await testSubjects.exists('euiFlyoutCloseButton'))
|
||||
await testSubjects.click('euiFlyoutCloseButton', 60 * 1000);
|
||||
},
|
||||
|
||||
async alreadyHasJob() {
|
||||
return await testSubjects.exists('uptimeManageMLJobBtn');
|
||||
},
|
||||
|
@ -55,5 +61,19 @@ export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) {
|
|||
async hasNoLicenseInfo() {
|
||||
return await testSubjects.missingOrFail('uptimeMLLicenseInfo', { timeout: 1000 });
|
||||
},
|
||||
|
||||
async openAlertFlyout() {
|
||||
return await testSubjects.click('uptimeEnableAnomalyAlertBtn');
|
||||
},
|
||||
|
||||
async disableAnomalyAlertIsVisible() {
|
||||
return await testSubjects.exists('uptimeDisableAnomalyAlertBtn');
|
||||
},
|
||||
|
||||
async changeAlertThreshold(level: string) {
|
||||
await testSubjects.click('uptimeAnomalySeverity');
|
||||
await testSubjects.click('anomalySeveritySelect');
|
||||
await testSubjects.click(`alertAnomaly${level}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
131
x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts
Normal file
131
x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
describe('uptime anomaly alert', () => {
|
||||
const pageObjects = getPageObjects(['common', 'uptime']);
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
|
||||
const monitorId = '0000-intermittent';
|
||||
|
||||
const uptime = getService('uptime');
|
||||
|
||||
const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078';
|
||||
const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078';
|
||||
let alerts: any;
|
||||
const alertId = 'uptime-anomaly-alert';
|
||||
|
||||
before(async () => {
|
||||
alerts = getService('uptime').alerts;
|
||||
|
||||
await uptime.navigation.goToUptime();
|
||||
|
||||
await uptime.navigation.loadDataAndGoToMonitorPage(
|
||||
DEFAULT_DATE_START,
|
||||
DEFAULT_DATE_END,
|
||||
monitorId
|
||||
);
|
||||
});
|
||||
|
||||
it('can delete existing job', async () => {
|
||||
if (await uptime.ml.alreadyHasJob()) {
|
||||
await uptime.ml.openMLManageMenu();
|
||||
await uptime.ml.deleteMLJob();
|
||||
await uptime.navigation.refreshApp();
|
||||
}
|
||||
});
|
||||
|
||||
it('can open ml flyout', async () => {
|
||||
await uptime.ml.openMLFlyout();
|
||||
});
|
||||
|
||||
it('has permission to create job', async () => {
|
||||
expect(uptime.ml.canCreateJob()).to.eql(true);
|
||||
expect(uptime.ml.hasNoLicenseInfo()).to.eql(false);
|
||||
});
|
||||
|
||||
it('can create job successfully', async () => {
|
||||
await uptime.ml.createMLJob();
|
||||
await pageObjects.common.closeToast();
|
||||
await uptime.ml.cancelAlertFlyout();
|
||||
});
|
||||
|
||||
it('can open ML Manage Menu', async () => {
|
||||
await uptime.ml.openMLManageMenu();
|
||||
});
|
||||
|
||||
it('can open anomaly alert flyout', async () => {
|
||||
await uptime.ml.openAlertFlyout();
|
||||
});
|
||||
|
||||
it('can set alert name', async () => {
|
||||
await alerts.setAlertName(alertId);
|
||||
});
|
||||
|
||||
it('can set alert tags', async () => {
|
||||
await alerts.setAlertTags(['uptime', 'anomaly-alert']);
|
||||
});
|
||||
|
||||
it('can change anomaly alert threshold', async () => {
|
||||
await uptime.ml.changeAlertThreshold('major');
|
||||
});
|
||||
|
||||
it('can save alert', async () => {
|
||||
await alerts.clickSaveAlertButton();
|
||||
await pageObjects.common.closeToast();
|
||||
});
|
||||
|
||||
it('has created a valid alert with expected parameters', async () => {
|
||||
let alert: any;
|
||||
await retry.tryForTime(15000, async () => {
|
||||
const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`);
|
||||
const alertsFromThisTest = apiResponse.body.data.filter(
|
||||
({ name }: { name: string }) => name === alertId
|
||||
);
|
||||
expect(alertsFromThisTest).to.have.length(1);
|
||||
alert = alertsFromThisTest[0];
|
||||
});
|
||||
|
||||
// Ensure the parameters and other stateful data
|
||||
// on the alert match up with the values we provided
|
||||
// for our test helper to input into the flyout.
|
||||
const { actions, alertTypeId, consumer, id, params, tags } = alert;
|
||||
try {
|
||||
expect(actions).to.eql([]);
|
||||
expect(alertTypeId).to.eql('xpack.uptime.alerts.durationAnomaly');
|
||||
expect(consumer).to.eql('uptime');
|
||||
expect(tags).to.eql(['uptime', 'anomaly-alert']);
|
||||
expect(params.monitorId).to.eql(monitorId);
|
||||
expect(params.severity).to.eql(50);
|
||||
} finally {
|
||||
await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204);
|
||||
}
|
||||
});
|
||||
|
||||
it('change button to disable anomaly alert', async () => {
|
||||
await uptime.ml.openMLManageMenu();
|
||||
expect(uptime.ml.disableAnomalyAlertIsVisible()).to.eql(true);
|
||||
});
|
||||
|
||||
it('can delete job successfully', async () => {
|
||||
await uptime.ml.deleteMLJob();
|
||||
});
|
||||
|
||||
it('verifies that alert is also deleted', async () => {
|
||||
await retry.tryForTime(15000, async () => {
|
||||
const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`);
|
||||
const alertsFromThisTest = apiResponse.body.data.filter(
|
||||
({ name }: { name: string }) => name === alertId
|
||||
);
|
||||
expect(alertsFromThisTest).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -22,6 +22,7 @@ export default ({ getService, loadTestFile }: FtrProviderContext) => {
|
|||
after(async () => await esArchiver.unload(ARCHIVE));
|
||||
|
||||
loadTestFile(require.resolve('./alert_flyout'));
|
||||
loadTestFile(require.resolve('./anomaly_alert'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue