[Uptime] Duration Anomaly Alert (#71208)
This commit is contained in:
parent
8f8736cce8
commit
981d678e42
|
@ -25,7 +25,14 @@ export function getResultsServiceProvider({
|
||||||
}: SharedServicesChecks): ResultsServiceProvider {
|
}: SharedServicesChecks): ResultsServiceProvider {
|
||||||
return {
|
return {
|
||||||
resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) {
|
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);
|
const { getAnomaliesTableData } = resultsServiceProvider(mlClusterClient);
|
||||||
return {
|
return {
|
||||||
async getAnomaliesTableData(...args) {
|
async getAnomaliesTableData(...args) {
|
||||||
|
|
|
@ -20,9 +20,14 @@ export const ACTION_GROUP_DEFINITIONS: ActionGroupDefinitions = {
|
||||||
id: 'xpack.uptime.alerts.actionGroups.tls',
|
id: 'xpack.uptime.alerts.actionGroups.tls',
|
||||||
name: 'Uptime TLS Alert',
|
name: 'Uptime TLS Alert',
|
||||||
},
|
},
|
||||||
|
DURATION_ANOMALY: {
|
||||||
|
id: 'xpack.uptime.alerts.actionGroups.durationAnomaly',
|
||||||
|
name: 'Uptime Duration Anomaly',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CLIENT_ALERT_TYPES = {
|
export const CLIENT_ALERT_TYPES = {
|
||||||
MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus',
|
MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus',
|
||||||
TLS: 'xpack.uptime.alerts.tls',
|
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_DELETE_JOB = `/api/ml/jobs/delete_jobs`,
|
||||||
ML_CAPABILITIES = '/api/ml/ml_capabilities',
|
ML_CAPABILITIES = '/api/ml/ml_capabilities',
|
||||||
ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`,
|
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.
|
* 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', () => {
|
describe('ML Anomaly API', () => {
|
||||||
it('it generates a lowercase job id', async () => {
|
it('it generates a lowercase job id', async () => {
|
|
@ -6,3 +6,5 @@
|
||||||
|
|
||||||
export * from './combine_filters_and_user_search';
|
export * from './combine_filters_and_user_search';
|
||||||
export * from './stringify_kueries';
|
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"],
|
"configPath": ["xpack", "uptime"],
|
||||||
"id": "uptime",
|
"id": "uptime",
|
||||||
"kibanaVersion": "kibana",
|
"kibanaVersion": "kibana",
|
||||||
"optionalPlugins": ["capabilities", "data", "home", "observability"],
|
"optionalPlugins": ["capabilities", "data", "home", "observability", "ml"],
|
||||||
"requiredPlugins": [
|
"requiredPlugins": [
|
||||||
"alerts",
|
"alerts",
|
||||||
"embeddable",
|
"embeddable",
|
||||||
|
|
|
@ -11,8 +11,8 @@ import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
||||||
|
|
||||||
describe('Manage ML Job', () => {
|
describe('Manage ML Job', () => {
|
||||||
it('shallow renders without errors', () => {
|
it('shallow renders without errors', () => {
|
||||||
const spy = jest.spyOn(redux, 'useSelector');
|
jest.spyOn(redux, 'useSelector').mockReturnValue(true);
|
||||||
spy.mockReturnValue(true);
|
jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn());
|
||||||
|
|
||||||
const wrapper = shallowWithRouter(
|
const wrapper = shallowWithRouter(
|
||||||
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
|
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
|
||||||
|
@ -21,8 +21,8 @@ describe('Manage ML Job', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders without errors', () => {
|
it('renders without errors', () => {
|
||||||
const spy = jest.spyOn(redux, 'useSelector');
|
jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn());
|
||||||
spy.mockReturnValue(true);
|
jest.spyOn(redux, 'useSelector').mockReturnValue(true);
|
||||||
|
|
||||||
const wrapper = renderWithRouter(
|
const wrapper = renderWithRouter(
|
||||||
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
|
<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 React, { useContext, useState } from 'react';
|
||||||
|
|
||||||
import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
|
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 {
|
import {
|
||||||
canDeleteMLJobSelector,
|
canDeleteMLJobSelector,
|
||||||
hasMLJobSelector,
|
hasMLJobSelector,
|
||||||
|
@ -18,6 +19,10 @@ import * as labels from './translations';
|
||||||
import { getMLJobLinkHref } from './ml_job_link';
|
import { getMLJobLinkHref } from './ml_job_link';
|
||||||
import { useGetUrlParams } from '../../../hooks';
|
import { useGetUrlParams } from '../../../hooks';
|
||||||
import { useMonitorId } 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 {
|
interface Props {
|
||||||
hasMLJob: boolean;
|
hasMLJob: boolean;
|
||||||
|
@ -40,6 +45,15 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
|
||||||
|
|
||||||
const monitorId = useMonitorId();
|
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 = (
|
const button = (
|
||||||
<EuiButtonEmpty
|
<EuiButtonEmpty
|
||||||
data-test-subj={hasMLJob ? 'uptimeManageMLJobBtn' : 'uptimeEnableAnomalyBtn'}
|
data-test-subj={hasMLJob ? 'uptimeManageMLJobBtn' : 'uptimeEnableAnomalyBtn'}
|
||||||
|
@ -68,6 +82,21 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
|
||||||
}),
|
}),
|
||||||
target: '_blank',
|
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,
|
name: labels.DISABLE_ANOMALY_DETECTION,
|
||||||
'data-test-subj': 'uptimeDeleteMLJobBtn',
|
'data-test-subj': 'uptimeDeleteMLJobBtn',
|
||||||
|
@ -82,12 +111,29 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPopover button={button} isOpen={isPopOverOpen} closePopover={() => setIsPopOverOpen(false)}>
|
<>
|
||||||
<EuiContextMenu
|
<EuiPopover
|
||||||
initialPanelId={0}
|
button={button}
|
||||||
panels={panels}
|
isOpen={isPopOverOpen}
|
||||||
data-test-subj="uptimeManageMLContextMenu"
|
closePopover={() => setIsPopOverOpen(false)}
|
||||||
/>
|
>
|
||||||
</EuiPopover>
|
<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,
|
isMLJobCreatingSelector,
|
||||||
selectDynamicSettings,
|
selectDynamicSettings,
|
||||||
} from '../../../state/selectors';
|
} from '../../../state/selectors';
|
||||||
import { createMLJobAction, getExistingMLJobAction } from '../../../state/actions';
|
import {
|
||||||
|
createMLJobAction,
|
||||||
|
getExistingMLJobAction,
|
||||||
|
setAlertFlyoutType,
|
||||||
|
setAlertFlyoutVisible,
|
||||||
|
} from '../../../state/actions';
|
||||||
import { MLJobLink } from './ml_job_link';
|
import { MLJobLink } from './ml_job_link';
|
||||||
import * as labels from './translations';
|
import * as labels from './translations';
|
||||||
import {
|
|
||||||
useKibana,
|
|
||||||
KibanaReactNotifications,
|
|
||||||
} from '../../../../../../../src/plugins/kibana_react/public';
|
|
||||||
import { MLFlyoutView } from './ml_flyout';
|
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 { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts';
|
||||||
import { useGetUrlParams } from '../../../hooks';
|
import { useGetUrlParams } from '../../../hooks';
|
||||||
import { getDynamicSettings } from '../../../state/actions/dynamic_settings';
|
import { getDynamicSettings } from '../../../state/actions/dynamic_settings';
|
||||||
import { useMonitorId } from '../../../hooks';
|
import { useMonitorId } from '../../../hooks';
|
||||||
|
import { kibanaService } from '../../../state/kibana_service';
|
||||||
|
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showMLJobNotification = (
|
const showMLJobNotification = (
|
||||||
notifications: KibanaReactNotifications,
|
|
||||||
monitorId: string,
|
monitorId: string,
|
||||||
basePath: string,
|
basePath: string,
|
||||||
range: { to: string; from: string },
|
range: { to: string; from: string },
|
||||||
success: boolean,
|
success: boolean,
|
||||||
message = ''
|
error?: Error
|
||||||
) => {
|
) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
notifications.toasts.success({
|
kibanaService.toasts.addSuccess(
|
||||||
title: (
|
{
|
||||||
<p data-test-subj="uptimeMLJobSuccessfullyCreated">{labels.JOB_CREATED_SUCCESS_TITLE}</p>
|
title: toMountPoint(
|
||||||
),
|
<p data-test-subj="uptimeMLJobSuccessfullyCreated">{labels.JOB_CREATED_SUCCESS_TITLE}</p>
|
||||||
body: (
|
),
|
||||||
<p>
|
text: toMountPoint(
|
||||||
{labels.JOB_CREATED_SUCCESS_MESSAGE}
|
<p>
|
||||||
<MLJobLink monitorId={monitorId} basePath={basePath} dateRange={range}>
|
{labels.JOB_CREATED_SUCCESS_MESSAGE}
|
||||||
{labels.VIEW_JOB}
|
<MLJobLink monitorId={monitorId} basePath={basePath} dateRange={range}>
|
||||||
</MLJobLink>
|
{labels.VIEW_JOB}
|
||||||
</p>
|
</MLJobLink>
|
||||||
),
|
</p>
|
||||||
toastLifeTimeMs: 10000,
|
),
|
||||||
});
|
},
|
||||||
|
{ toastLifeTimeMs: 10000 }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
notifications.toasts.danger({
|
kibanaService.toasts.addError(error!, {
|
||||||
title: <p data-test-subj="uptimeMLJobCreationFailed">{labels.JOB_CREATION_FAILED}</p>,
|
title: labels.JOB_CREATION_FAILED,
|
||||||
body: message ?? <p>{labels.JOB_CREATION_FAILED_MESSAGE}</p>,
|
toastMessage: labels.JOB_CREATION_FAILED_MESSAGE,
|
||||||
toastLifeTimeMs: 10000,
|
toastLifeTimeMs: 10000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
||||||
const { notifications } = useKibana();
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { data: hasMLJob, error } = useSelector(hasNewMLJobSelector);
|
const { data: hasMLJob, error } = useSelector(hasNewMLJobSelector);
|
||||||
const isMLJobCreating = useSelector(isMLJobCreatingSelector);
|
const isMLJobCreating = useSelector(isMLJobCreatingSelector);
|
||||||
|
@ -100,7 +102,6 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
||||||
if (isCreatingJob && !isMLJobCreating) {
|
if (isCreatingJob && !isMLJobCreating) {
|
||||||
if (hasMLJob) {
|
if (hasMLJob) {
|
||||||
showMLJobNotification(
|
showMLJobNotification(
|
||||||
notifications,
|
|
||||||
monitorId as string,
|
monitorId as string,
|
||||||
basePath,
|
basePath,
|
||||||
{ to: dateRangeEnd, from: dateRangeStart },
|
{ to: dateRangeEnd, from: dateRangeStart },
|
||||||
|
@ -112,31 +113,22 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
||||||
loadMLJob(ML_JOB_ID);
|
loadMLJob(ML_JOB_ID);
|
||||||
|
|
||||||
refreshApp();
|
refreshApp();
|
||||||
|
dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY));
|
||||||
|
dispatch(setAlertFlyoutVisible(true));
|
||||||
} else {
|
} else {
|
||||||
showMLJobNotification(
|
showMLJobNotification(
|
||||||
notifications,
|
|
||||||
monitorId as string,
|
monitorId as string,
|
||||||
basePath,
|
basePath,
|
||||||
{ to: dateRangeEnd, from: dateRangeStart },
|
{ to: dateRangeEnd, from: dateRangeStart },
|
||||||
false,
|
false,
|
||||||
error?.message || error?.body?.message
|
error as Error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setIsCreatingJob(false);
|
setIsCreatingJob(false);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [hasMLJob, onClose, isCreatingJob, error, isMLJobCreating, monitorId, dispatch, basePath]);
|
||||||
hasMLJob,
|
|
||||||
notifications,
|
|
||||||
onClose,
|
|
||||||
isCreatingJob,
|
|
||||||
error,
|
|
||||||
isMLJobCreating,
|
|
||||||
monitorId,
|
|
||||||
dispatch,
|
|
||||||
basePath,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExistingMLJob && !isMLJobCreating && !hasMLJob && heartbeatIndices) {
|
if (hasExistingMLJob && !isMLJobCreating && !hasMLJob && heartbeatIndices) {
|
||||||
|
|
|
@ -16,12 +16,12 @@ import {
|
||||||
import { deleteMLJobAction, getExistingMLJobAction, resetMLState } from '../../../state/actions';
|
import { deleteMLJobAction, getExistingMLJobAction, resetMLState } from '../../../state/actions';
|
||||||
import { ConfirmJobDeletion } from './confirm_delete';
|
import { ConfirmJobDeletion } from './confirm_delete';
|
||||||
import { UptimeRefreshContext } from '../../../contexts';
|
import { UptimeRefreshContext } from '../../../contexts';
|
||||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
|
||||||
import * as labels from './translations';
|
import * as labels from './translations';
|
||||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||||
import { ManageMLJobComponent } from './manage_ml_job';
|
import { ManageMLJobComponent } from './manage_ml_job';
|
||||||
import { JobStat } from '../../../../../../plugins/ml/public';
|
import { JobStat } from '../../../../../../plugins/ml/public';
|
||||||
import { useMonitorId } from '../../../hooks';
|
import { useMonitorId } from '../../../hooks';
|
||||||
|
import { getMLJobId } from '../../../../common/lib';
|
||||||
|
|
||||||
export const MLIntegrationComponent = () => {
|
export const MLIntegrationComponent = () => {
|
||||||
const [isMlFlyoutOpen, setIsMlFlyoutOpen] = useState(false);
|
const [isMlFlyoutOpen, setIsMlFlyoutOpen] = useState(false);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import { EuiButtonEmpty } from '@elastic/eui';
|
import { EuiButtonEmpty } from '@elastic/eui';
|
||||||
import rison, { RisonValue } from 'rison-node';
|
import rison, { RisonValue } from 'rison-node';
|
||||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
import { getMLJobId } from '../../../../common/lib';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
monitorId: string;
|
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(
|
export const MANAGE_ANOMALY_DETECTION = i18n.translate(
|
||||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.manageAnomalyDetectionTitle',
|
'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,
|
selectDurationLines,
|
||||||
} from '../../../state/selectors';
|
} from '../../../state/selectors';
|
||||||
import { UptimeRefreshContext } from '../../../contexts';
|
import { UptimeRefreshContext } from '../../../contexts';
|
||||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
|
||||||
import { JobStat } from '../../../../../ml/public';
|
import { JobStat } from '../../../../../ml/public';
|
||||||
import { MonitorDurationComponent } from './monitor_duration';
|
import { MonitorDurationComponent } from './monitor_duration';
|
||||||
import { MonitorIdParam } from '../../../../common/types';
|
import { MonitorIdParam } from '../../../../common/types';
|
||||||
|
import { getMLJobId } from '../../../../common/lib';
|
||||||
|
|
||||||
export const MonitorDuration: React.FC<MonitorIdParam> = ({ monitorId }) => {
|
export const MonitorDuration: React.FC<MonitorIdParam> = ({ monitorId }) => {
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -14,8 +14,8 @@ interface AlertExpressionPopoverProps {
|
||||||
'data-test-subj': string;
|
'data-test-subj': string;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
|
value: string | JSX.Element;
|
||||||
isInvalid?: boolean;
|
isInvalid?: boolean;
|
||||||
value: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getColor = (
|
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 { initMonitorStatusAlertType } from './monitor_status';
|
||||||
import { initTlsAlertType } from './tls';
|
import { initTlsAlertType } from './tls';
|
||||||
import { ClientPluginsStart } from '../../apps/plugin';
|
import { ClientPluginsStart } from '../../apps/plugin';
|
||||||
|
import { initDurationAnomalyAlertType } from './duration_anomaly';
|
||||||
|
|
||||||
export type AlertTypeInitializer = (dependenies: {
|
export type AlertTypeInitializer = (dependenies: {
|
||||||
core: CoreStart;
|
core: CoreStart;
|
||||||
|
@ -18,4 +19,5 @@ export type AlertTypeInitializer = (dependenies: {
|
||||||
export const alertTypeInitializers: AlertTypeInitializer[] = [
|
export const alertTypeInitializers: AlertTypeInitializer[] = [
|
||||||
initMonitorStatusAlertType,
|
initMonitorStatusAlertType,
|
||||||
initTlsAlertType,
|
initTlsAlertType,
|
||||||
|
initDurationAnomalyAlertType,
|
||||||
];
|
];
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const TlsTranslations = {
|
||||||
{expiringConditionalOpen}
|
{expiringConditionalOpen}
|
||||||
Expiring cert count: {expiringCount}
|
Expiring cert count: {expiringCount}
|
||||||
Expiring Certificates: {expiringCommonNameAndDate}
|
Expiring Certificates: {expiringCommonNameAndDate}
|
||||||
{expiringConditionalClose}
|
{expiringConditionalClose}
|
||||||
|
|
||||||
{agingConditionalOpen}
|
{agingConditionalOpen}
|
||||||
Aging cert count: {agingCount}
|
Aging cert count: {agingCount}
|
||||||
|
@ -49,3 +49,23 @@ Aging Certificates: {agingCommonNameAndDate}
|
||||||
defaultMessage: 'Uptime TLS',
|
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 { MonitorStatusDetails, PingList } from '../components/monitor';
|
||||||
import { getDynamicSettings } from '../state/actions/dynamic_settings';
|
import { getDynamicSettings } from '../state/actions/dynamic_settings';
|
||||||
import { Ping } from '../../common/runtime_types/ping';
|
import { Ping } from '../../common/runtime_types/ping';
|
||||||
|
import { setSelectedMonitorId } from '../state/actions';
|
||||||
|
|
||||||
const isAutogeneratedId = (id: string) => {
|
const isAutogeneratedId = (id: string) => {
|
||||||
const autoGeneratedId = /^auto-(icmp|http|tcp)-OX[A-F0-9]{16}-[a-f0-9]{16}/;
|
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();
|
const monitorId = useMonitorId();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setSelectedMonitorId(monitorId));
|
||||||
|
}, [monitorId, dispatch]);
|
||||||
|
|
||||||
const selectedMonitor = useSelector(monitorStatusSelector);
|
const selectedMonitor = useSelector(monitorStatusSelector);
|
||||||
|
|
||||||
useTrackPageview({ app: 'uptime', path: 'monitor' });
|
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>(
|
export const toggleIntegrationsPopover = createAction<PopoverState>(
|
||||||
'TOGGLE INTEGRATION POPOVER STATE'
|
'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 moment from 'moment';
|
||||||
import { apiService } from './utils';
|
import { apiService } from './utils';
|
||||||
import { AnomalyRecords, AnomalyRecordsParams } from '../actions';
|
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 {
|
import {
|
||||||
MlCapabilitiesResponse,
|
|
||||||
DataRecognizerConfigResponse,
|
DataRecognizerConfigResponse,
|
||||||
JobExistResult,
|
JobExistResult,
|
||||||
|
MlCapabilitiesResponse,
|
||||||
} from '../../../../../plugins/ml/public';
|
} from '../../../../../plugins/ml/public';
|
||||||
import {
|
import {
|
||||||
CreateMLJobSuccess,
|
CreateMLJobSuccess,
|
||||||
DeleteJobResults,
|
DeleteJobResults,
|
||||||
MonitorIdParam,
|
|
||||||
HeartbeatIndicesParam,
|
HeartbeatIndicesParam,
|
||||||
|
MonitorIdParam,
|
||||||
} from '../actions/types';
|
} from '../actions/types';
|
||||||
|
import { getJobPrefix, getMLJobId } from '../../../common/lib/ml';
|
||||||
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}`;
|
|
||||||
|
|
||||||
export const getMLCapabilities = async (): Promise<MlCapabilitiesResponse> => {
|
export const getMLCapabilities = async (): Promise<MlCapabilitiesResponse> => {
|
||||||
return await apiService.get(API_URLS.ML_CAPABILITIES);
|
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 { fetchMLJobEffect } from './ml_anomaly';
|
||||||
import { fetchIndexStatusEffect } from './index_status';
|
import { fetchIndexStatusEffect } from './index_status';
|
||||||
import { fetchCertificatesEffect } from '../certificates/certificates';
|
import { fetchCertificatesEffect } from '../certificates/certificates';
|
||||||
|
import { fetchAlertsEffect } from './alerts';
|
||||||
|
|
||||||
export function* rootEffect() {
|
export function* rootEffect() {
|
||||||
yield fork(fetchMonitorDetailsEffect);
|
yield fork(fetchMonitorDetailsEffect);
|
||||||
|
@ -33,4 +34,5 @@ export function* rootEffect() {
|
||||||
yield fork(fetchMonitorDurationEffect);
|
yield fork(fetchMonitorDurationEffect);
|
||||||
yield fork(fetchIndexStatusEffect);
|
yield fork(fetchIndexStatusEffect);
|
||||||
yield fork(fetchCertificatesEffect);
|
yield fork(fetchCertificatesEffect);
|
||||||
|
yield fork(fetchAlertsEffect);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 {
|
import {
|
||||||
getMLCapabilitiesAction,
|
getMLCapabilitiesAction,
|
||||||
getExistingMLJobAction,
|
getExistingMLJobAction,
|
||||||
|
@ -20,6 +21,9 @@ import {
|
||||||
deleteMLJob,
|
deleteMLJob,
|
||||||
getMLCapabilities,
|
getMLCapabilities,
|
||||||
} from '../api/ml_anomaly';
|
} from '../api/ml_anomaly';
|
||||||
|
import { deleteAlertAction } from '../actions/alerts';
|
||||||
|
import { alertSelector } from '../selectors';
|
||||||
|
import { MonitorIdParam } from '../actions/types';
|
||||||
|
|
||||||
export function* fetchMLJobEffect() {
|
export function* fetchMLJobEffect() {
|
||||||
yield takeLatest(
|
yield takeLatest(
|
||||||
|
@ -38,10 +42,22 @@ export function* fetchMLJobEffect() {
|
||||||
getAnomalyRecordsAction.fail
|
getAnomalyRecordsAction.fail
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
yield takeLatest(
|
|
||||||
deleteMLJobAction.get,
|
yield takeLatest(String(deleteMLJobAction.get), function* (action: Action<MonitorIdParam>) {
|
||||||
fetchEffectFactory(deleteMLJob, deleteMLJobAction.success, deleteMLJobAction.fail)
|
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(
|
yield takeLatest(
|
||||||
getMLCapabilitiesAction.get,
|
getMLCapabilitiesAction.get,
|
||||||
fetchEffectFactory(
|
fetchEffectFactory(
|
||||||
|
|
|
@ -20,6 +20,10 @@ class KibanaService {
|
||||||
apiService.http = this._core.http;
|
apiService.http = this._core.http;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get toasts() {
|
||||||
|
return this._core.notifications.toasts;
|
||||||
|
}
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
static getInstance(): KibanaService {
|
static getInstance(): KibanaService {
|
||||||
|
|
|
@ -9,6 +9,7 @@ Object {
|
||||||
"id": "popover-2",
|
"id": "popover-2",
|
||||||
"open": true,
|
"open": true,
|
||||||
},
|
},
|
||||||
|
"monitorId": "test",
|
||||||
"searchText": "",
|
"searchText": "",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -19,6 +20,7 @@ Object {
|
||||||
"basePath": "yyz",
|
"basePath": "yyz",
|
||||||
"esKuery": "",
|
"esKuery": "",
|
||||||
"integrationsPopoverOpen": null,
|
"integrationsPopoverOpen": null,
|
||||||
|
"monitorId": "test",
|
||||||
"searchText": "",
|
"searchText": "",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -24,6 +24,7 @@ describe('ui reducer', () => {
|
||||||
esKuery: '',
|
esKuery: '',
|
||||||
integrationsPopoverOpen: null,
|
integrationsPopoverOpen: null,
|
||||||
searchText: '',
|
searchText: '',
|
||||||
|
monitorId: 'test',
|
||||||
},
|
},
|
||||||
action
|
action
|
||||||
)
|
)
|
||||||
|
@ -43,6 +44,7 @@ describe('ui reducer', () => {
|
||||||
esKuery: '',
|
esKuery: '',
|
||||||
integrationsPopoverOpen: null,
|
integrationsPopoverOpen: null,
|
||||||
searchText: '',
|
searchText: '',
|
||||||
|
monitorId: 'test',
|
||||||
},
|
},
|
||||||
action
|
action
|
||||||
)
|
)
|
||||||
|
@ -59,6 +61,7 @@ describe('ui reducer', () => {
|
||||||
esKuery: '',
|
esKuery: '',
|
||||||
integrationsPopoverOpen: null,
|
integrationsPopoverOpen: null,
|
||||||
searchText: '',
|
searchText: '',
|
||||||
|
monitorId: 'test',
|
||||||
},
|
},
|
||||||
action
|
action
|
||||||
)
|
)
|
||||||
|
@ -68,6 +71,7 @@ describe('ui reducer', () => {
|
||||||
"basePath": "",
|
"basePath": "",
|
||||||
"esKuery": "",
|
"esKuery": "",
|
||||||
"integrationsPopoverOpen": null,
|
"integrationsPopoverOpen": null,
|
||||||
|
"monitorId": "test",
|
||||||
"searchText": "",
|
"searchText": "",
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -83,6 +87,7 @@ describe('ui reducer', () => {
|
||||||
esKuery: '',
|
esKuery: '',
|
||||||
integrationsPopoverOpen: null,
|
integrationsPopoverOpen: null,
|
||||||
searchText: '',
|
searchText: '',
|
||||||
|
monitorId: 'test',
|
||||||
},
|
},
|
||||||
action
|
action
|
||||||
)
|
)
|
||||||
|
@ -92,6 +97,7 @@ describe('ui reducer', () => {
|
||||||
"basePath": "",
|
"basePath": "",
|
||||||
"esKuery": "",
|
"esKuery": "",
|
||||||
"integrationsPopoverOpen": null,
|
"integrationsPopoverOpen": null,
|
||||||
|
"monitorId": "test",
|
||||||
"searchText": "lorem ipsum",
|
"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 { mlJobsReducer } from './ml_anomaly';
|
||||||
import { certificatesReducer } from '../certificates/certificates';
|
import { certificatesReducer } from '../certificates/certificates';
|
||||||
import { selectedFiltersReducer } from './selected_filters';
|
import { selectedFiltersReducer } from './selected_filters';
|
||||||
|
import { alertsReducer } from './alerts';
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
monitor: monitorReducer,
|
monitor: monitorReducer,
|
||||||
|
@ -37,4 +38,5 @@ export const rootReducer = combineReducers({
|
||||||
indexStatus: indexStatusReducer,
|
indexStatus: indexStatusReducer,
|
||||||
certificates: certificatesReducer,
|
certificates: certificatesReducer,
|
||||||
selectedFilters: selectedFiltersReducer,
|
selectedFilters: selectedFiltersReducer,
|
||||||
|
alerts: alertsReducer,
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
setAlertFlyoutType,
|
setAlertFlyoutType,
|
||||||
setAlertFlyoutVisible,
|
setAlertFlyoutVisible,
|
||||||
setSearchTextAction,
|
setSearchTextAction,
|
||||||
|
setSelectedMonitorId,
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
|
|
||||||
export interface UiState {
|
export interface UiState {
|
||||||
|
@ -23,6 +24,7 @@ export interface UiState {
|
||||||
esKuery: string;
|
esKuery: string;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
integrationsPopoverOpen: PopoverState | null;
|
integrationsPopoverOpen: PopoverState | null;
|
||||||
|
monitorId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UiState = {
|
const initialState: UiState = {
|
||||||
|
@ -31,6 +33,7 @@ const initialState: UiState = {
|
||||||
esKuery: '',
|
esKuery: '',
|
||||||
searchText: '',
|
searchText: '',
|
||||||
integrationsPopoverOpen: null,
|
integrationsPopoverOpen: null,
|
||||||
|
monitorId: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uiReducer = handleActions<UiState, UiPayload>(
|
export const uiReducer = handleActions<UiState, UiPayload>(
|
||||||
|
@ -64,6 +67,10 @@ export const uiReducer = handleActions<UiState, UiPayload>(
|
||||||
...state,
|
...state,
|
||||||
searchText: action.payload,
|
searchText: action.payload,
|
||||||
}),
|
}),
|
||||||
|
[String(setSelectedMonitorId)]: (state, action: Action<string>) => ({
|
||||||
|
...state,
|
||||||
|
monitorId: action.payload,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,6 +45,7 @@ describe('state selectors', () => {
|
||||||
esKuery: '',
|
esKuery: '',
|
||||||
integrationsPopoverOpen: null,
|
integrationsPopoverOpen: null,
|
||||||
searchText: '',
|
searchText: '',
|
||||||
|
monitorId: '',
|
||||||
},
|
},
|
||||||
monitorStatus: {
|
monitorStatus: {
|
||||||
status: null,
|
status: null,
|
||||||
|
@ -108,6 +109,10 @@ describe('state selectors', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selectedFilters: null,
|
selectedFilters: null,
|
||||||
|
alerts: {
|
||||||
|
alertDeletion: { data: null, loading: false },
|
||||||
|
alert: { data: null, loading: false },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it('selects base path from state', () => {
|
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 isMLJobCreatingSelector = ({ ml }: AppState) => ml.createJob.loading;
|
||||||
|
|
||||||
export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading;
|
export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading;
|
||||||
|
export const isAnomalyAlertDeletingSelector = ({ alerts }: AppState) =>
|
||||||
|
alerts.alertDeletion.loading;
|
||||||
|
|
||||||
export const isMLJobDeletedSelector = ({ ml }: AppState) => ml.deleteJob;
|
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 searchTextSelector = ({ ui: { searchText } }: AppState) => searchText;
|
||||||
|
|
||||||
export const selectedFiltersSelector = ({ selectedFilters }: AppState) => selectedFilters;
|
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 { UMKibanaRoute } from '../../../rest_api';
|
||||||
import { PluginSetupContract } from '../../../../../features/server';
|
import { PluginSetupContract } from '../../../../../features/server';
|
||||||
import { DynamicSettings } from '../../../../common/runtime_types';
|
import { DynamicSettings } from '../../../../common/runtime_types';
|
||||||
|
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
|
||||||
|
|
||||||
export type APICaller = (
|
export type APICaller = (
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
@ -39,6 +40,7 @@ export interface UptimeCorePlugins {
|
||||||
alerts: any;
|
alerts: any;
|
||||||
elasticsearch: any;
|
elasticsearch: any;
|
||||||
usageCollection: UsageCollectionSetup;
|
usageCollection: UsageCollectionSetup;
|
||||||
|
ml: MlSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UMBackendFrameworkAdapter {
|
export interface UMBackendFrameworkAdapter {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { GetMonitorStatusResult } from '../../requests';
|
||||||
import { AlertType } from '../../../../../alerts/server';
|
import { AlertType } from '../../../../../alerts/server';
|
||||||
import { IRouter } from 'kibana/server';
|
import { IRouter } from 'kibana/server';
|
||||||
import { UMServerLibs } from '../../lib';
|
import { UMServerLibs } from '../../lib';
|
||||||
import { UptimeCoreSetup } from '../../adapters';
|
import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters';
|
||||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
|
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
|
||||||
import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks';
|
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
|
// these server/libs parameters don't have any functionality, which is fine
|
||||||
// because we aren't testing them here
|
// because we aren't testing them here
|
||||||
const server: UptimeCoreSetup = { router };
|
const server: UptimeCoreSetup = { router };
|
||||||
|
const plugins: UptimeCorePlugins = {} as any;
|
||||||
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
|
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
|
||||||
libs.requests = { ...libs.requests, ...customRequests };
|
libs.requests = { ...libs.requests, ...customRequests };
|
||||||
return { server, libs };
|
return { server, libs, plugins };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,8 +83,8 @@ describe('status check alert', () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
const mockGetter = jest.fn();
|
const mockGetter = jest.fn();
|
||||||
mockGetter.mockReturnValue([]);
|
mockGetter.mockReturnValue([]);
|
||||||
const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
// @ts-ignore the executor can return `void`, but ours never does
|
// @ts-ignore the executor can return `void`, but ours never does
|
||||||
const state: Record<string, any> = await alert.executor(mockOptions());
|
const state: Record<string, any> = await alert.executor(mockOptions());
|
||||||
|
|
||||||
|
@ -128,8 +129,8 @@ describe('status check alert', () => {
|
||||||
status: 'down',
|
status: 'down',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
const options = mockOptions();
|
const options = mockOptions();
|
||||||
const alertServices: AlertServicesMock = options.services;
|
const alertServices: AlertServicesMock = options.services;
|
||||||
// @ts-ignore the executor can return `void`, but ours never does
|
// @ts-ignore the executor can return `void`, but ours never does
|
||||||
|
@ -213,11 +214,11 @@ describe('status check alert', () => {
|
||||||
status: 'down',
|
status: 'down',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const { server, libs } = bootstrapDependencies({
|
const { server, libs, plugins } = bootstrapDependencies({
|
||||||
getMonitorStatus: mockGetter,
|
getMonitorStatus: mockGetter,
|
||||||
getIndexPattern: jest.fn(),
|
getIndexPattern: jest.fn(),
|
||||||
});
|
});
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
const options = mockOptions({
|
const options = mockOptions({
|
||||||
numTimes: 4,
|
numTimes: 4,
|
||||||
timerange: { from: 'now-14h', to: 'now' },
|
timerange: { from: 'now-14h', to: 'now' },
|
||||||
|
@ -286,11 +287,11 @@ describe('status check alert', () => {
|
||||||
status: 'down',
|
status: 'down',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const { server, libs } = bootstrapDependencies({
|
const { server, libs, plugins } = bootstrapDependencies({
|
||||||
getMonitorStatus: mockGetter,
|
getMonitorStatus: mockGetter,
|
||||||
getIndexPattern: jest.fn(),
|
getIndexPattern: jest.fn(),
|
||||||
});
|
});
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
const options = mockOptions({
|
const options = mockOptions({
|
||||||
numTimes: 3,
|
numTimes: 3,
|
||||||
timerangeUnit: 'm',
|
timerangeUnit: 'm',
|
||||||
|
@ -371,11 +372,11 @@ describe('status check alert', () => {
|
||||||
toISOStringSpy.mockImplementation(() => 'search test');
|
toISOStringSpy.mockImplementation(() => 'search test');
|
||||||
const mockGetter = jest.fn();
|
const mockGetter = jest.fn();
|
||||||
mockGetter.mockReturnValue([]);
|
mockGetter.mockReturnValue([]);
|
||||||
const { server, libs } = bootstrapDependencies({
|
const { server, libs, plugins } = bootstrapDependencies({
|
||||||
getIndexPattern: jest.fn(),
|
getIndexPattern: jest.fn(),
|
||||||
getMonitorStatus: mockGetter,
|
getMonitorStatus: mockGetter,
|
||||||
});
|
});
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
const options = mockOptions({
|
const options = mockOptions({
|
||||||
numTimes: 20,
|
numTimes: 20,
|
||||||
timerangeCount: 30,
|
timerangeCount: 30,
|
||||||
|
@ -467,12 +468,12 @@ describe('status check alert', () => {
|
||||||
availabilityRatio: 0.909245845760545,
|
availabilityRatio: 0.909245845760545,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const { server, libs } = bootstrapDependencies({
|
const { server, libs, plugins } = bootstrapDependencies({
|
||||||
getMonitorAvailability: mockAvailability,
|
getMonitorAvailability: mockAvailability,
|
||||||
getMonitorStatus: mockGetter,
|
getMonitorStatus: mockGetter,
|
||||||
getIndexPattern: jest.fn(),
|
getIndexPattern: jest.fn(),
|
||||||
});
|
});
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
const options = mockOptions({
|
const options = mockOptions({
|
||||||
availability: {
|
availability: {
|
||||||
range: 35,
|
range: 35,
|
||||||
|
@ -559,11 +560,11 @@ describe('status check alert', () => {
|
||||||
mockGetter.mockReturnValue([]);
|
mockGetter.mockReturnValue([]);
|
||||||
const mockAvailability = jest.fn();
|
const mockAvailability = jest.fn();
|
||||||
mockAvailability.mockReturnValue([]);
|
mockAvailability.mockReturnValue([]);
|
||||||
const { server, libs } = bootstrapDependencies({
|
const { server, libs, plugins } = bootstrapDependencies({
|
||||||
getMonitorAvailability: mockAvailability,
|
getMonitorAvailability: mockAvailability,
|
||||||
getIndexPattern: jest.fn(),
|
getIndexPattern: jest.fn(),
|
||||||
});
|
});
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
const options = mockOptions({
|
const options = mockOptions({
|
||||||
availability: {
|
availability: {
|
||||||
range: 23,
|
range: 23,
|
||||||
|
@ -600,11 +601,11 @@ describe('status check alert', () => {
|
||||||
mockGetter.mockReturnValue([]);
|
mockGetter.mockReturnValue([]);
|
||||||
const mockAvailability = jest.fn();
|
const mockAvailability = jest.fn();
|
||||||
mockAvailability.mockReturnValue([]);
|
mockAvailability.mockReturnValue([]);
|
||||||
const { server, libs } = bootstrapDependencies({
|
const { server, libs, plugins } = bootstrapDependencies({
|
||||||
getMonitorAvailability: mockAvailability,
|
getMonitorAvailability: mockAvailability,
|
||||||
getIndexPattern: jest.fn(),
|
getIndexPattern: jest.fn(),
|
||||||
});
|
});
|
||||||
const alert = statusCheckAlertFactory(server, libs);
|
const alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
const options = mockOptions({
|
const options = mockOptions({
|
||||||
availability: {
|
availability: {
|
||||||
range: 23,
|
range: 23,
|
||||||
|
@ -748,8 +749,8 @@ describe('status check alert', () => {
|
||||||
let alert: AlertType;
|
let alert: AlertType;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const { server, libs } = bootstrapDependencies();
|
const { server, libs, plugins } = bootstrapDependencies();
|
||||||
alert = statusCheckAlertFactory(server, libs);
|
alert = statusCheckAlertFactory(server, libs, plugins);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates an alert with expected params', () => {
|
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 { UptimeAlertTypeFactory } from './types';
|
||||||
import { statusCheckAlertFactory } from './status_check';
|
import { statusCheckAlertFactory } from './status_check';
|
||||||
import { tlsAlertFactory } from './tls';
|
import { tlsAlertFactory } from './tls';
|
||||||
|
import { durationAnomalyAlertFactory } from './duration_anomaly';
|
||||||
|
|
||||||
export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [
|
export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [
|
||||||
statusCheckAlertFactory,
|
statusCheckAlertFactory,
|
||||||
tlsAlertFactory,
|
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 { AlertType } from '../../../../alerts/server';
|
||||||
import { UptimeCoreSetup } from '../adapters';
|
import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters';
|
||||||
import { UMServerLibs } from '../lib';
|
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) =>
|
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() {
|
async openMLManageMenu() {
|
||||||
|
await this.cancelAlertFlyout();
|
||||||
return retry.tryForTime(30000, async () => {
|
return retry.tryForTime(30000, async () => {
|
||||||
await testSubjects.click('uptimeManageMLJobBtn');
|
await testSubjects.click('uptimeManageMLJobBtn');
|
||||||
await testSubjects.existOrFail('uptimeManageMLContextMenu');
|
await testSubjects.existOrFail('uptimeManageMLContextMenu');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async cancelAlertFlyout() {
|
||||||
|
if (await testSubjects.exists('euiFlyoutCloseButton'))
|
||||||
|
await testSubjects.click('euiFlyoutCloseButton', 60 * 1000);
|
||||||
|
},
|
||||||
|
|
||||||
async alreadyHasJob() {
|
async alreadyHasJob() {
|
||||||
return await testSubjects.exists('uptimeManageMLJobBtn');
|
return await testSubjects.exists('uptimeManageMLJobBtn');
|
||||||
},
|
},
|
||||||
|
@ -55,5 +61,19 @@ export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) {
|
||||||
async hasNoLicenseInfo() {
|
async hasNoLicenseInfo() {
|
||||||
return await testSubjects.missingOrFail('uptimeMLLicenseInfo', { timeout: 1000 });
|
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));
|
after(async () => await esArchiver.unload(ARCHIVE));
|
||||||
|
|
||||||
loadTestFile(require.resolve('./alert_flyout'));
|
loadTestFile(require.resolve('./alert_flyout'));
|
||||||
|
loadTestFile(require.resolve('./anomaly_alert'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue