[Uptime] TLS alerting (#63913)
* Refactor settings form event handling and modify certs fields. * Fix/improve broken types/unit/integration/api tests. * Modify default expiration threshold. * Rename test vars. * Implement PR feedback. * Refresh snapshots, fix broken tests/types. * Remove unnecessary state spreading. * Add type for settings field errors. * Refresh test snapshots. * Improve punctuation. * Add TLS alert type. * Add cert API request and runtime type checking. * Add api test for cert api. * Add unload command to certs test. * Extract API params interface to io-ts type. * Add TLS alert type on server. * WIP - add state for changing selected alert type. * Finish adding alert type for client, add server alert summary. * Add some state variables. * Update certs summary function to create required values. * Refresh test snapshots. * Clean up message generator function. * Add a comment. * Update formatting for alert messages, add flags denoting presence of age/expiration data. * Add relative date information to tls alert messages. * Clean up more logic in certs request function. * Fix broken unit tests. * Move tests for common function to new file. * Fix logic error in test and add common state fields to tls alerts. * Extract common state field translations from status check alert. * Add a comment. * Add nested context navigation for uptime alert selection. * Clean up types. * Fix translation key typo. * Extract translations from tls alert factory. * Extract summary messages to translation file. * Change default tls alert time window from 1w to 1d. * Remove unnecessary import. * Simplify page linking. * Extract a non-trivial component to a dedicated file. * Simplify create alert copy. * Fix broken functional test. * Fix busted types. * Fix tls query error. * Allow for alerts toggle button to receive a set of types to display. * Add alerts toggle button to certs page. * Fix copy. * Fixup punctuation in default message to avoid double-period symbols. * Refresh snapshots.
This commit is contained in:
parent
418804d6ec
commit
6d78489c14
|
@ -16,4 +16,13 @@ export const ACTION_GROUP_DEFINITIONS: ActionGroupDefinitions = {
|
|||
id: 'xpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
name: 'Uptime Down Monitor',
|
||||
},
|
||||
TLS: {
|
||||
id: 'xpack.uptime.alerts.actionGroups.tls',
|
||||
name: 'Uptime TLS Alert',
|
||||
},
|
||||
};
|
||||
|
||||
export const CLIENT_ALERT_TYPES = {
|
||||
MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus',
|
||||
TLS: 'xpack.uptime.alerts.tls',
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ACTION_GROUP_DEFINITIONS } from './alerts';
|
||||
export * from './alerts';
|
||||
export { CHART_FORMAT_LIMITS } from './chart_format_limits';
|
||||
export { CLIENT_DEFAULTS } from './client_defaults';
|
||||
export { CONTEXT_DEFAULTS } from './context_defaults';
|
||||
|
|
23
x-pack/plugins/uptime/common/runtime_types/alerts/common.ts
Normal file
23
x-pack/plugins/uptime/common/runtime_types/alerts/common.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
|
||||
export const UptimeCommonStateType = t.intersection([
|
||||
t.partial({
|
||||
currentTriggerStarted: t.string,
|
||||
firstTriggeredAt: t.string,
|
||||
lastTriggeredAt: t.string,
|
||||
lastResolvedAt: t.string,
|
||||
}),
|
||||
t.type({
|
||||
firstCheckedAt: t.string,
|
||||
lastCheckedAt: t.string,
|
||||
isTriggered: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type UptimeCommonState = t.TypeOf<typeof UptimeCommonStateType>;
|
|
@ -4,9 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
StatusCheckAlertStateType,
|
||||
StatusCheckAlertState,
|
||||
StatusCheckExecutorParamsType,
|
||||
StatusCheckExecutorParams,
|
||||
} from './status_check';
|
||||
export * from './common';
|
||||
export * from './status_check';
|
||||
|
|
|
@ -6,22 +6,6 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const StatusCheckAlertStateType = t.intersection([
|
||||
t.partial({
|
||||
currentTriggerStarted: t.string,
|
||||
firstTriggeredAt: t.string,
|
||||
lastTriggeredAt: t.string,
|
||||
lastResolvedAt: t.string,
|
||||
}),
|
||||
t.type({
|
||||
firstCheckedAt: t.string,
|
||||
lastCheckedAt: t.string,
|
||||
isTriggered: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type StatusCheckAlertState = t.TypeOf<typeof StatusCheckAlertStateType>;
|
||||
|
||||
export const StatusCheckExecutorParamsType = t.intersection([
|
||||
t.partial({
|
||||
filters: t.string,
|
||||
|
|
|
@ -15,6 +15,8 @@ export const GetCertsParamsType = t.intersection([
|
|||
}),
|
||||
t.partial({
|
||||
search: t.string,
|
||||
notValidBefore: t.string,
|
||||
notValidAfter: t.string,
|
||||
from: t.string,
|
||||
to: t.string,
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { TlsTranslations } from './translations';
|
||||
import { SettingsMessageExpressionPopover } from './settings_message_expression_popover';
|
||||
|
||||
interface Props {
|
||||
ageThreshold?: number;
|
||||
expirationThreshold?: number;
|
||||
setAlertFlyoutVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const AlertTlsComponent: React.FC<Props> = props => (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiExpression
|
||||
aria-label={TlsTranslations.criteriaAriaLabel}
|
||||
color="secondary"
|
||||
description={TlsTranslations.criteriaDescription}
|
||||
value={TlsTranslations.criteriaValue}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SettingsMessageExpressionPopover
|
||||
aria-label={TlsTranslations.expirationAriaLabel}
|
||||
id="expiration"
|
||||
description={TlsTranslations.expirationDescription}
|
||||
value={TlsTranslations.expirationValue(props.expirationThreshold)}
|
||||
{...props}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SettingsMessageExpressionPopover
|
||||
aria-label={TlsTranslations.ageAriaLabel}
|
||||
id="age"
|
||||
description={TlsTranslations.ageDescription}
|
||||
value={TlsTranslations.ageValue(props.ageThreshold)}
|
||||
{...props}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
);
|
|
@ -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 { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { useCallback } from 'react';
|
||||
import { AlertTlsComponent } from '../alert_tls';
|
||||
import { setAlertFlyoutVisible } from '../../../../state/actions';
|
||||
import { selectDynamicSettings } from '../../../../state/selectors';
|
||||
|
||||
export const AlertTls = () => {
|
||||
const dispatch = useDispatch();
|
||||
const setFlyoutVisible = useCallback((value: boolean) => dispatch(setAlertFlyoutVisible(value)), [
|
||||
dispatch,
|
||||
]);
|
||||
const { settings } = useSelector(selectDynamicSettings);
|
||||
return (
|
||||
<AlertTlsComponent
|
||||
ageThreshold={settings?.certThresholds?.age}
|
||||
expirationThreshold={settings?.certThresholds?.expiration}
|
||||
setAlertFlyoutVisible={setFlyoutVisible}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,5 +5,8 @@
|
|||
*/
|
||||
|
||||
export { AlertMonitorStatus } from './alert_monitor_status';
|
||||
export { ToggleAlertFlyoutButton } from './toggle_alert_flyout_button';
|
||||
export {
|
||||
ToggleAlertFlyoutButton,
|
||||
ToggleAlertFlyoutButtonProps,
|
||||
} from './toggle_alert_flyout_button';
|
||||
export { UptimeAlertsFlyoutWrapper } from './uptime_alerts_flyout_wrapper';
|
||||
|
|
|
@ -6,14 +6,26 @@
|
|||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setAlertFlyoutVisible } from '../../../../state/actions';
|
||||
import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../../state/actions';
|
||||
import { ToggleAlertFlyoutButtonComponent } from '../index';
|
||||
|
||||
export const ToggleAlertFlyoutButton = () => {
|
||||
export interface ToggleAlertFlyoutButtonProps {
|
||||
alertOptions?: string[];
|
||||
}
|
||||
|
||||
export const ToggleAlertFlyoutButton: React.FC<ToggleAlertFlyoutButtonProps> = props => {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<ToggleAlertFlyoutButtonComponent
|
||||
setAlertFlyoutVisible={(value: boolean) => dispatch(setAlertFlyoutVisible(value))}
|
||||
{...props}
|
||||
setAlertFlyoutVisible={(value: boolean | string) => {
|
||||
if (typeof value === 'string') {
|
||||
dispatch(setAlertFlyoutType(value));
|
||||
dispatch(setAlertFlyoutVisible(true));
|
||||
} else {
|
||||
dispatch(setAlertFlyoutVisible(value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,27 +7,22 @@
|
|||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setAlertFlyoutVisible } from '../../../../state/actions';
|
||||
import { selectAlertFlyoutVisibility } from '../../../../state/selectors';
|
||||
import { UptimeAlertsFlyoutWrapperComponent } from '../index';
|
||||
import { UptimeAlertsFlyoutWrapperComponent } from '../uptime_alerts_flyout_wrapper';
|
||||
import { selectAlertFlyoutVisibility, selectAlertFlyoutType } from '../../../../state/selectors';
|
||||
|
||||
interface Props {
|
||||
alertTypeId?: string;
|
||||
canChangeTrigger?: boolean;
|
||||
}
|
||||
|
||||
export const UptimeAlertsFlyoutWrapper = ({ alertTypeId, canChangeTrigger }: Props) => {
|
||||
export const UptimeAlertsFlyoutWrapper: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const setAddFlyoutVisibility = (value: React.SetStateAction<boolean>) =>
|
||||
// @ts-ignore the value here is a boolean, and it works with the action creator function
|
||||
dispatch(setAlertFlyoutVisible(value));
|
||||
|
||||
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
|
||||
const alertTypeId = useSelector(selectAlertFlyoutType);
|
||||
|
||||
return (
|
||||
<UptimeAlertsFlyoutWrapperComponent
|
||||
alertFlyoutVisible={alertFlyoutVisible}
|
||||
alertTypeId={alertTypeId}
|
||||
canChangeTrigger={canChangeTrigger}
|
||||
setAlertFlyoutVisibility={setAddFlyoutVisibility}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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, EuiPopover } from '@elastic/eui';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { SETTINGS_ROUTE } from '../../../../common/constants';
|
||||
|
||||
interface SettingsMessageExpressionPopoverProps {
|
||||
'aria-label': string;
|
||||
description: string;
|
||||
id: string;
|
||||
setAlertFlyoutVisible: (value: boolean) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressionPopoverProps> = ({
|
||||
'aria-label': ariaLabel,
|
||||
description,
|
||||
setAlertFlyoutVisible,
|
||||
value,
|
||||
id,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<EuiPopover
|
||||
id={id}
|
||||
anchorPosition="downLeft"
|
||||
button={
|
||||
<EuiExpression
|
||||
aria-label={ariaLabel}
|
||||
color="secondary"
|
||||
description={description}
|
||||
isActive={isOpen}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
value={value}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.alerts.tls.settingsPageNav.text"
|
||||
defaultMessage="You can edit these thresholds on the {settingsPageLink}."
|
||||
values={{
|
||||
settingsPageLink: (
|
||||
// this link is wrapped around a span so we can also change the UI state
|
||||
// and hide the alert flyout before triggering the navigation to the settings page
|
||||
<Link to={SETTINGS_ROUTE} data-test-subj="xpack.uptime.alerts.tlsFlyout.linkToSettings">
|
||||
<span
|
||||
onClick={() => {
|
||||
setAlertFlyoutVisible(false);
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setAlertFlyoutVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
settings page
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -4,27 +4,128 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiContextMenuPanelItemDescriptor,
|
||||
EuiLink,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants';
|
||||
import { ToggleFlyoutTranslations } from './translations';
|
||||
import { ToggleAlertFlyoutButtonProps } from './alerts_containers';
|
||||
|
||||
interface Props {
|
||||
setAlertFlyoutVisible: (value: boolean) => void;
|
||||
interface ComponentProps {
|
||||
setAlertFlyoutVisible: (value: boolean | string) => void;
|
||||
}
|
||||
|
||||
export const ToggleAlertFlyoutButtonComponent = ({ setAlertFlyoutVisible }: Props) => {
|
||||
type Props = ComponentProps & ToggleAlertFlyoutButtonProps;
|
||||
|
||||
const ALERT_CONTEXT_MAIN_PANEL_ID = 0;
|
||||
const ALERT_CONTEXT_SELECT_TYPE_PANEL_ID = 1;
|
||||
|
||||
export const ToggleAlertFlyoutButtonComponent: React.FC<Props> = ({
|
||||
alertOptions,
|
||||
setAlertFlyoutVisible,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const kibana = useKibana();
|
||||
const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
|
||||
'data-test-subj': 'xpack.uptime.toggleAlertFlyout',
|
||||
name: ToggleFlyoutTranslations.toggleMonitorStatusContent,
|
||||
onClick: () => {
|
||||
setAlertFlyoutVisible(CLIENT_ALERT_TYPES.MONITOR_STATUS);
|
||||
setIsOpen(false);
|
||||
},
|
||||
};
|
||||
|
||||
const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.toggleTlsAriaLabel,
|
||||
'data-test-subj': 'xpack.uptime.toggleTlsAlertFlyout',
|
||||
name: ToggleFlyoutTranslations.toggleTlsContent,
|
||||
onClick: () => {
|
||||
setAlertFlyoutVisible(CLIENT_ALERT_TYPES.TLS);
|
||||
setIsOpen(false);
|
||||
},
|
||||
};
|
||||
|
||||
const managementContextItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
|
||||
'data-test-subj': 'xpack.uptime.navigateToAlertingUi',
|
||||
name: (
|
||||
<EuiLink
|
||||
color="text"
|
||||
href={kibana.services?.application?.getUrlForApp(
|
||||
'kibana#/management/kibana/triggersActions/alerts'
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.navigateToAlertingButton.content"
|
||||
defaultMessage="Manage alerts"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
icon: 'tableOfContents',
|
||||
};
|
||||
|
||||
let selectionItems: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
if (!alertOptions) {
|
||||
selectionItems = [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem];
|
||||
} else {
|
||||
alertOptions.forEach(option => {
|
||||
if (option === CLIENT_ALERT_TYPES.MONITOR_STATUS)
|
||||
selectionItems.push(monitorStatusAlertContextMenuItem);
|
||||
else if (option === CLIENT_ALERT_TYPES.TLS) selectionItems.push(tlsAlertContextMenuItem);
|
||||
});
|
||||
}
|
||||
|
||||
if (selectionItems.length === 1) {
|
||||
selectionItems[0].icon = 'bell';
|
||||
}
|
||||
|
||||
let panels: EuiContextMenuPanelDescriptor[];
|
||||
if (selectionItems.length === 1) {
|
||||
panels = [
|
||||
{
|
||||
id: ALERT_CONTEXT_MAIN_PANEL_ID,
|
||||
title: 'main panel',
|
||||
items: [...selectionItems, managementContextItem],
|
||||
},
|
||||
];
|
||||
} else {
|
||||
panels = [
|
||||
{
|
||||
id: ALERT_CONTEXT_MAIN_PANEL_ID,
|
||||
title: 'main panel',
|
||||
items: [
|
||||
{
|
||||
'aria-label': ToggleFlyoutTranslations.openAlertContextPanelAriaLabel,
|
||||
'data-test-subj': 'xpack.uptime.openAlertContextPanel',
|
||||
name: ToggleFlyoutTranslations.openAlertContextPanelLabel,
|
||||
icon: 'bell',
|
||||
panel: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
|
||||
},
|
||||
managementContextItem,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
|
||||
title: 'create alerts',
|
||||
items: selectionItems,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
aria-label={i18n.translate('xpack.uptime.alertsPopover.toggleButton.ariaLabel', {
|
||||
defaultMessage: 'Open alert context menu',
|
||||
})}
|
||||
aria-label={ToggleFlyoutTranslations.toggleButtonAriaLabel}
|
||||
data-test-subj="xpack.uptime.alertsPopover.toggleButton"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
|
@ -40,43 +141,7 @@ export const ToggleAlertFlyoutButtonComponent = ({ setAlertFlyoutVisible }: Prop
|
|||
isOpen={isOpen}
|
||||
ownFocus
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
aria-label={i18n.translate('xpack.uptime.toggleAlertFlyout.ariaLabel', {
|
||||
defaultMessage: 'Open add alert flyout',
|
||||
})}
|
||||
data-test-subj="xpack.uptime.toggleAlertFlyout"
|
||||
key="create-alert"
|
||||
icon="bell"
|
||||
onClick={() => {
|
||||
setAlertFlyoutVisible(true);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.toggleAlertButton.content"
|
||||
defaultMessage="Create alert"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
aria-label={i18n.translate('xpack.uptime.navigateToAlertingUi', {
|
||||
defaultMessage: 'Leave Uptime and go to Alerting Management page',
|
||||
})}
|
||||
data-test-subj="xpack.uptime.navigateToAlertingUi"
|
||||
icon="tableOfContents"
|
||||
key="navigate-to-alerting"
|
||||
href={kibana.services?.application?.getUrlForApp(
|
||||
'kibana#/management/kibana/triggersActions/alerts'
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.navigateToAlertingButton.content"
|
||||
defaultMessage="Manage alerts"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 TlsTranslations = {
|
||||
criteriaAriaLabel: i18n.translate('xpack.uptime.alerts.tls.criteriaExpression.ariaLabel', {
|
||||
defaultMessage:
|
||||
'An expression displaying the criteria for monitor that are watched by this alert',
|
||||
}),
|
||||
criteriaDescription: i18n.translate('xpack.uptime.alerts.tls.criteriaExpression.description', {
|
||||
defaultMessage: 'when',
|
||||
description:
|
||||
'The context of this `when` is in the conditional sense, like "when there are three cookies, eat them all".',
|
||||
}),
|
||||
criteriaValue: i18n.translate('xpack.uptime.alerts.tls.criteriaExpression.value', {
|
||||
defaultMessage: 'any monitor',
|
||||
}),
|
||||
expirationAriaLabel: i18n.translate('xpack.uptime.alerts.tls.expirationExpression.ariaLabel', {
|
||||
defaultMessage:
|
||||
'An expression displaying the threshold that will trigger the TLS alert for certificate expiration',
|
||||
}),
|
||||
expirationDescription: i18n.translate(
|
||||
'xpack.uptime.alerts.tls.expirationExpression.description',
|
||||
{
|
||||
defaultMessage: 'has a certificate expiring within',
|
||||
}
|
||||
),
|
||||
expirationValue: (value?: number) =>
|
||||
i18n.translate('xpack.uptime.alerts.tls.expirationExpression.value', {
|
||||
defaultMessage: '{value} days',
|
||||
values: { value },
|
||||
}),
|
||||
ageAriaLabel: i18n.translate('xpack.uptime.alerts.tls.ageExpression.ariaLabel', {
|
||||
defaultMessage:
|
||||
'An expressing displaying the threshold that will trigger the TLS alert for old certificates',
|
||||
}),
|
||||
ageDescription: i18n.translate('xpack.uptime.alerts.tls.ageExpression.description', {
|
||||
defaultMessage: 'or older than',
|
||||
}),
|
||||
ageValue: (value?: number) =>
|
||||
i18n.translate('xpack.uptime.alerts.tls.ageExpression.value', {
|
||||
defaultMessage: '{value} days',
|
||||
values: { value },
|
||||
}),
|
||||
};
|
||||
|
||||
export const ToggleFlyoutTranslations = {
|
||||
toggleButtonAriaLabel: i18n.translate('xpack.uptime.alertsPopover.toggleButton.ariaLabel', {
|
||||
defaultMessage: 'Open alert context menu',
|
||||
}),
|
||||
openAlertContextPanelAriaLabel: i18n.translate('xpack.uptime.openAlertContextPanel.ariaLabel', {
|
||||
defaultMessage: 'Open the alert context panel so you can choose an alert type',
|
||||
}),
|
||||
openAlertContextPanelLabel: i18n.translate('xpack.uptime.openAlertContextPanel.label', {
|
||||
defaultMessage: 'Create alert',
|
||||
}),
|
||||
toggleTlsAriaLabel: i18n.translate('xpack.uptime.toggleTlsAlertButton.ariaLabel', {
|
||||
defaultMessage: 'Open TLS alert flyout',
|
||||
}),
|
||||
toggleTlsContent: i18n.translate('xpack.uptime.toggleTlsAlertButton.content', {
|
||||
defaultMessage: 'TLS alert',
|
||||
}),
|
||||
toggleMonitorStatusAriaLabel: i18n.translate('xpack.uptime.toggleAlertFlyout.ariaLabel', {
|
||||
defaultMessage: 'Open add alert flyout',
|
||||
}),
|
||||
toggleMonitorStatusContent: i18n.translate('xpack.uptime.toggleAlertButton.content', {
|
||||
defaultMessage: 'Monitor status alert',
|
||||
}),
|
||||
navigateToAlertingUIAriaLabel: i18n.translate('xpack.uptime.navigateToAlertingUi', {
|
||||
defaultMessage: 'Leave Uptime and go to Alerting Management page',
|
||||
}),
|
||||
navigateToAlertingButtonContent: i18n.translate('xpack.uptime.navigateToAlertingButton.content', {
|
||||
defaultMessage: 'Manage alerts',
|
||||
}),
|
||||
};
|
|
@ -10,14 +10,12 @@ import { AlertAdd } from '../../../../../../plugins/triggers_actions_ui/public';
|
|||
interface Props {
|
||||
alertFlyoutVisible: boolean;
|
||||
alertTypeId?: string;
|
||||
canChangeTrigger?: boolean;
|
||||
setAlertFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const UptimeAlertsFlyoutWrapperComponent = ({
|
||||
alertFlyoutVisible,
|
||||
alertTypeId,
|
||||
canChangeTrigger,
|
||||
setAlertFlyoutVisibility,
|
||||
}: Props) => (
|
||||
<AlertAdd
|
||||
|
@ -25,6 +23,8 @@ export const UptimeAlertsFlyoutWrapperComponent = ({
|
|||
consumer="uptime"
|
||||
setAddFlyoutVisibility={setAlertFlyoutVisibility}
|
||||
alertTypeId={alertTypeId}
|
||||
canChangeTrigger={canChangeTrigger}
|
||||
// if we don't have an alert type pre-specified, we need to
|
||||
// let the user choose
|
||||
canChangeTrigger={!alertTypeId}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -4,20 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { useCallback } from 'react';
|
||||
import { DataPublicPluginSetup } from 'src/plugins/data/public';
|
||||
import { OverviewPageComponent } from '../../pages/overview';
|
||||
import { selectIndexPattern } from '../../state/selectors';
|
||||
import { AppState } from '../../state';
|
||||
import { setEsKueryString } from '../../state/actions';
|
||||
|
||||
interface DispatchProps {
|
||||
setEsKueryFilters: typeof setEsKueryString;
|
||||
export interface OverviewPageProps {
|
||||
autocomplete: DataPublicPluginSetup['autocomplete'];
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any): DispatchProps => ({
|
||||
setEsKueryFilters: (esFilters: string) => dispatch(setEsKueryString(esFilters)),
|
||||
});
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({ ...selectIndexPattern(state) });
|
||||
|
||||
export const OverviewPage = connect(mapStateToProps, mapDispatchToProps)(OverviewPageComponent);
|
||||
export const OverviewPage: React.FC<OverviewPageProps> = props => {
|
||||
const dispatch = useDispatch();
|
||||
const setEsKueryFilters = useCallback(
|
||||
(esFilters: string) => dispatch(setEsKueryString(esFilters)),
|
||||
[dispatch]
|
||||
);
|
||||
const indexPattern = useSelector(selectIndexPattern);
|
||||
return (
|
||||
<OverviewPageComponent setEsKueryFilters={setEsKueryFilters} {...indexPattern} {...props} />
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import { initMonitorStatusAlertType } from './monitor_status';
|
||||
import { initTlsAlertType } from './tls';
|
||||
|
||||
export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel;
|
||||
|
||||
export const alertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType];
|
||||
export const alertTypeInitializers: AlertTypeInitializer[] = [
|
||||
initMonitorStatusAlertType,
|
||||
initTlsAlertType,
|
||||
];
|
||||
|
|
|
@ -12,6 +12,8 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
|||
import { AlertTypeInitializer } from '.';
|
||||
import { StatusCheckExecutorParamsType } from '../../../common/runtime_types';
|
||||
import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
|
||||
import { MonitorStatusTranslations } from './translations';
|
||||
|
||||
export const validate = (alertParams: any) => {
|
||||
const errors: Record<string, any> = {};
|
||||
|
@ -52,15 +54,15 @@ export const validate = (alertParams: any) => {
|
|||
return { errors };
|
||||
};
|
||||
|
||||
const { name, defaultActionMessage } = MonitorStatusTranslations;
|
||||
|
||||
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
|
||||
autocomplete,
|
||||
}): AlertTypeModel => ({
|
||||
id: 'xpack.uptime.alerts.monitorStatus',
|
||||
name: 'Uptime monitor status',
|
||||
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
|
||||
name,
|
||||
iconClass: 'uptimeApp',
|
||||
alertParamsExpression: params => {
|
||||
return <AlertMonitorStatus {...params} autocomplete={autocomplete} />;
|
||||
},
|
||||
alertParamsExpression: params => <AlertMonitorStatus {...params} autocomplete={autocomplete} />,
|
||||
validate,
|
||||
defaultActionMessage: `{{context.message}}\nLast triggered at: {{state.lastTriggeredAt}}\n{{context.downMonitorsWithGeo}}`,
|
||||
defaultActionMessage,
|
||||
});
|
||||
|
|
23
x-pack/plugins/uptime/public/lib/alert_types/tls.tsx
Normal file
23
x-pack/plugins/uptime/public/lib/alert_types/tls.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
|
||||
import { TlsTranslations } from './translations';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
import { AlertTls } from '../../components/overview/alerts/alerts_containers/alert_tls';
|
||||
|
||||
const { name, defaultActionMessage } = TlsTranslations;
|
||||
|
||||
export const initTlsAlertType: AlertTypeInitializer = (): AlertTypeModel => ({
|
||||
id: CLIENT_ALERT_TYPES.TLS,
|
||||
iconClass: 'uptimeApp',
|
||||
alertParamsExpression: () => <AlertTls />,
|
||||
name,
|
||||
validate: () => ({ errors: {} }),
|
||||
defaultActionMessage,
|
||||
});
|
52
x-pack/plugins/uptime/public/lib/alert_types/translations.ts
Normal file
52
x-pack/plugins/uptime/public/lib/alert_types/translations.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const MonitorStatusTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.monitorStatus.defaultActionMessage', {
|
||||
defaultMessage: '{contextMessage}\nLast triggered at: {lastTriggered}\n{downMonitors}',
|
||||
values: {
|
||||
contextMessage: '{{context.message}}',
|
||||
lastTriggered: '{{state.lastTriggeredAt}}',
|
||||
downMonitors: '{{context.downMonitorsWithGeo}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.monitorStatus.clientName', {
|
||||
defaultMessage: 'Uptime monitor status',
|
||||
}),
|
||||
};
|
||||
|
||||
export const TlsTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
|
||||
defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
|
||||
|
||||
{expiringConditionalOpen}
|
||||
Expiring cert count: {expiringCount}
|
||||
Expiring Certificates: {expiringCommonNameAndDate}
|
||||
{expiringConditionalClose}
|
||||
|
||||
{agingConditionalOpen}
|
||||
Aging cert count: {agingCount}
|
||||
Aging Certificates: {agingCommonNameAndDate}
|
||||
{agingConditionalClose}
|
||||
`,
|
||||
values: {
|
||||
count: '{{state.count}}',
|
||||
expiringCount: '{{state.expiringCount}}',
|
||||
expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
|
||||
expiringConditionalOpen: '{{#state.hasExpired}}',
|
||||
expiringConditionalClose: '{{/state.hasExpired}}',
|
||||
agingCount: '{{state.agingCount}}',
|
||||
agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
|
||||
agingConditionalOpen: '{{#state.hasAging}}',
|
||||
agingConditionalClose: '{{/state.hasAging}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
|
||||
defaultMessage: 'Uptime TLS',
|
||||
}),
|
||||
};
|
|
@ -19,13 +19,14 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { useTrackPageview } from '../../../observability/public';
|
||||
import { PageHeader } from './page_header';
|
||||
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
|
||||
import { OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../common/constants';
|
||||
import { OVERVIEW_ROUTE, SETTINGS_ROUTE, CLIENT_ALERT_TYPES } from '../../common/constants';
|
||||
import { getDynamicSettings } from '../state/actions/dynamic_settings';
|
||||
import { UptimeRefreshContext } from '../contexts';
|
||||
import * as labels from './translations';
|
||||
import { UptimePage, useUptimeTelemetry } from '../hooks';
|
||||
import { certificatesSelector, getCertificatesAction } from '../state/certificates/certificates';
|
||||
import { CertificateList, CertificateSearch, CertSort } from '../components/certificates';
|
||||
import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize';
|
||||
|
@ -83,6 +84,9 @@ export const CertificatesPage: React.FC = () => {
|
|||
</EuiButtonEmpty>
|
||||
</Link>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ToggleAlertFlyoutButton alertOptions={[CLIENT_ALERT_TYPES.TLS]} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||
<Link to={SETTINGS_ROUTE} data-test-subj="uptimeCertificatesToOverviewLink">
|
||||
<EuiButtonEmpty size="s" color="primary" iconType="gear">
|
||||
|
|
|
@ -11,22 +11,20 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useUptimeTelemetry, UptimePage, useGetUrlParams } from '../hooks';
|
||||
import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
|
||||
import { PageHeader } from './page_header';
|
||||
import { DataPublicPluginSetup, IIndexPattern } from '../../../../../src/plugins/data/public';
|
||||
import { IIndexPattern } from '../../../../../src/plugins/data/public';
|
||||
import { useUpdateKueryString } from '../hooks';
|
||||
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
|
||||
import { useTrackPageview } from '../../../observability/public';
|
||||
import { MonitorList } from '../components/overview/monitor_list/monitor_list_container';
|
||||
import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview';
|
||||
import { StatusPanel } from '../components/overview/status_panel';
|
||||
import { OverviewPageProps } from '../components/overview/overview_container';
|
||||
|
||||
interface OverviewPageProps {
|
||||
autocomplete: DataPublicPluginSetup['autocomplete'];
|
||||
interface Props extends OverviewPageProps {
|
||||
indexPattern: IIndexPattern | null;
|
||||
setEsKueryFilters: (esFilters: string) => void;
|
||||
}
|
||||
|
||||
type Props = OverviewPageProps;
|
||||
|
||||
const EuiFlexItemStyled = styled(EuiFlexItem)`
|
||||
&& {
|
||||
min-width: 598px;
|
||||
|
|
|
@ -12,7 +12,9 @@ export interface PopoverState {
|
|||
|
||||
export type UiPayload = PopoverState & string & number & Map<string, string[]>;
|
||||
|
||||
export const setAlertFlyoutVisible = createAction<boolean>('TOGGLE ALERT FLYOUT');
|
||||
export const setAlertFlyoutVisible = createAction<boolean | undefined>('TOGGLE ALERT FLYOUT');
|
||||
|
||||
export const setAlertFlyoutType = createAction<string>('SET ALERT FLYOUT TYPE');
|
||||
|
||||
export const setBasePath = createAction<string>('SET BASE PATH');
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@ import {
|
|||
setEsKueryString,
|
||||
triggerAppRefresh,
|
||||
UiPayload,
|
||||
setAlertFlyoutType,
|
||||
setAlertFlyoutVisible,
|
||||
} from '../actions';
|
||||
|
||||
export interface UiState {
|
||||
alertFlyoutVisible: boolean;
|
||||
alertFlyoutType?: string;
|
||||
basePath: string;
|
||||
esKuery: string;
|
||||
integrationsPopoverOpen: PopoverState | null;
|
||||
|
@ -57,6 +59,11 @@ export const uiReducer = handleActions<UiState, UiPayload>(
|
|||
...state,
|
||||
esKuery: action.payload as string,
|
||||
}),
|
||||
|
||||
[String(setAlertFlyoutType)]: (state, action: Action<string>) => ({
|
||||
...state,
|
||||
alertFlyoutType: action.payload,
|
||||
}),
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
|
|
@ -91,6 +91,8 @@ export const selectDurationLines = ({ monitorDuration }: AppState) => {
|
|||
export const selectAlertFlyoutVisibility = ({ ui: { alertFlyoutVisible } }: AppState) =>
|
||||
alertFlyoutVisible;
|
||||
|
||||
export const selectAlertFlyoutType = ({ ui: { alertFlyoutType } }: AppState) => alertFlyoutType;
|
||||
|
||||
export const selectMonitorStatusAlert = ({ indexPattern, overviewFilters, ui }: AppState) => ({
|
||||
filters: ui.esKuery,
|
||||
indexPattern: indexPattern.index_pattern,
|
||||
|
|
|
@ -105,10 +105,7 @@ const Application = (props: UptimeAppProps) => {
|
|||
<UptimeAlertsContextProvider>
|
||||
<EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp">
|
||||
<main>
|
||||
<UptimeAlertsFlyoutWrapper
|
||||
alertTypeId="xpack.uptime.alerts.monitorStatus"
|
||||
canChangeTrigger={false}
|
||||
/>
|
||||
<UptimeAlertsFlyoutWrapper />
|
||||
<PageRouter autocomplete={plugins.data.autocomplete} />
|
||||
</main>
|
||||
</EuiPage>
|
||||
|
|
180
x-pack/plugins/uptime/server/lib/alerts/__tests__/common.test.ts
Normal file
180
x-pack/plugins/uptime/server/lib/alerts/__tests__/common.test.ts
Normal file
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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 { updateState } from '../common';
|
||||
|
||||
describe('updateState', () => {
|
||||
let spy: jest.SpyInstance<string, []>;
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(Date.prototype, 'toISOString');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sets initial state values', () => {
|
||||
spy.mockImplementation(() => 'foo date string');
|
||||
const result = updateState({}, false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "foo date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "foo date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('updates the correct field in subsequent calls', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string');
|
||||
const firstState = updateState({}, false);
|
||||
const secondState = updateState(firstState, true);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "second date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('correctly marks resolution times', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string')
|
||||
.mockImplementationOnce(() => 'third date string');
|
||||
const firstState = updateState({}, true);
|
||||
const secondState = updateState(firstState, true);
|
||||
const thirdState = updateState(secondState, false);
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "first date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "first date string",
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "first date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
expect(thirdState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "third date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('correctly marks state fields across multiple triggers/resolutions', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string')
|
||||
.mockImplementationOnce(() => 'third date string')
|
||||
.mockImplementationOnce(() => 'fourth date string')
|
||||
.mockImplementationOnce(() => 'fifth date string');
|
||||
const firstState = updateState({}, false);
|
||||
const secondState = updateState(firstState, true);
|
||||
const thirdState = updateState(secondState, false);
|
||||
const fourthState = updateState(thirdState, true);
|
||||
const fifthState = updateState(fourthState, false);
|
||||
expect(spy).toHaveBeenCalledTimes(5);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "second date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
expect(thirdState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "third date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
expect(fourthState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "fourth date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "fourth date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "fourth date string",
|
||||
}
|
||||
`);
|
||||
expect(fifthState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "fifth date string",
|
||||
"lastResolvedAt": "fifth date string",
|
||||
"lastTriggeredAt": "fourth date string",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -7,7 +7,6 @@
|
|||
import {
|
||||
contextMessage,
|
||||
uniqueMonitorIds,
|
||||
updateState,
|
||||
statusCheckAlertFactory,
|
||||
fullListByIdAndLocation,
|
||||
} from '../status_check';
|
||||
|
@ -337,179 +336,6 @@ describe('status check alert', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('updateState', () => {
|
||||
let spy: jest.SpyInstance<string, []>;
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(Date.prototype, 'toISOString');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sets initial state values', () => {
|
||||
spy.mockImplementation(() => 'foo date string');
|
||||
const result = updateState({}, false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "foo date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "foo date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('updates the correct field in subsequent calls', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string');
|
||||
const firstState = updateState({}, false);
|
||||
const secondState = updateState(firstState, true);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "second date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('correctly marks resolution times', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string')
|
||||
.mockImplementationOnce(() => 'third date string');
|
||||
const firstState = updateState({}, true);
|
||||
const secondState = updateState(firstState, true);
|
||||
const thirdState = updateState(secondState, false);
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "first date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "first date string",
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "first date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
expect(thirdState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "third date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('correctly marks state fields across multiple triggers/resolutions', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string')
|
||||
.mockImplementationOnce(() => 'third date string')
|
||||
.mockImplementationOnce(() => 'fourth date string')
|
||||
.mockImplementationOnce(() => 'fifth date string');
|
||||
const firstState = updateState({}, false);
|
||||
const secondState = updateState(firstState, true);
|
||||
const thirdState = updateState(secondState, false);
|
||||
const fourthState = updateState(thirdState, true);
|
||||
const fifthState = updateState(fourthState, false);
|
||||
expect(spy).toHaveBeenCalledTimes(5);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "second date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
expect(thirdState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "third date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "second date string",
|
||||
}
|
||||
`);
|
||||
expect(fourthState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": "fourth date string",
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "fourth date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "fourth date string",
|
||||
}
|
||||
`);
|
||||
expect(fifthState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentTriggerStarted": undefined,
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "fifth date string",
|
||||
"lastResolvedAt": "fifth date string",
|
||||
"lastTriggeredAt": "fourth date string",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uniqueMonitorIds', () => {
|
||||
let items: GetMonitorStatusResult[];
|
||||
beforeEach(() => {
|
||||
|
|
147
x-pack/plugins/uptime/server/lib/alerts/__tests__/tls.test.ts
Normal file
147
x-pack/plugins/uptime/server/lib/alerts/__tests__/tls.test.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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 { getCertSummary } from '../tls';
|
||||
import { Cert } from '../../../../common/runtime_types';
|
||||
|
||||
describe('tls alert', () => {
|
||||
describe('getCertSummary', () => {
|
||||
let mockCerts: Cert[];
|
||||
let diffSpy: jest.SpyInstance<any, unknown[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
diffSpy = jest.spyOn(moment.prototype, 'diff');
|
||||
mockCerts = [
|
||||
{
|
||||
not_after: '2020-07-16T03:15:39.000Z',
|
||||
not_before: '2019-07-24T03:15:39.000Z',
|
||||
common_name: 'Common-One',
|
||||
monitors: [{ name: 'monitor-one', id: 'monitor1' }],
|
||||
sha256: 'abc',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-18T03:15:39.000Z',
|
||||
not_before: '2019-07-20T03:15:39.000Z',
|
||||
common_name: 'Common-Two',
|
||||
monitors: [{ name: 'monitor-two', id: 'monitor2' }],
|
||||
sha256: 'bcd',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-19T03:15:39.000Z',
|
||||
not_before: '2019-07-22T03:15:39.000Z',
|
||||
common_name: 'Common-Three',
|
||||
monitors: [{ name: 'monitor-three', id: 'monitor3' }],
|
||||
sha256: 'cde',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-25T03:15:39.000Z',
|
||||
not_before: '2019-07-25T03:15:39.000Z',
|
||||
common_name: 'Common-Four',
|
||||
monitors: [{ name: 'monitor-four', id: 'monitor4' }],
|
||||
sha256: 'def',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sorts expiring certs appropriately when creating summary', () => {
|
||||
diffSpy
|
||||
.mockReturnValueOnce(900)
|
||||
.mockReturnValueOnce(901)
|
||||
.mockReturnValueOnce(902);
|
||||
const result = getCertSummary(
|
||||
mockCerts,
|
||||
new Date('2020-07-20T05:00:00.000Z').valueOf(),
|
||||
new Date('2019-03-01T00:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"agingCommonNameAndDate": "",
|
||||
"agingCount": 0,
|
||||
"count": 4,
|
||||
"expiringCommonNameAndDate": "Common-One, expired on 2020-07-16T03:15:39.000Z 900 days ago.; Common-Two, expired on 2020-07-18T03:15:39.000Z 901 days ago.; Common-Three, expired on 2020-07-19T03:15:39.000Z 902 days ago.",
|
||||
"expiringCount": 3,
|
||||
"hasAging": null,
|
||||
"hasExpired": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sorts aging certs appropriate when creating summary', () => {
|
||||
diffSpy
|
||||
.mockReturnValueOnce(702)
|
||||
.mockReturnValueOnce(701)
|
||||
.mockReturnValueOnce(700);
|
||||
const result = getCertSummary(
|
||||
mockCerts,
|
||||
new Date('2020-07-01T12:00:00.000Z').valueOf(),
|
||||
new Date('2019-09-01T03:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 702 days ago.; Common-Three, valid since 2019-07-22T03:15:39.000Z, 701 days ago.; Common-One, valid since 2019-07-24T03:15:39.000Z, 700 days ago.",
|
||||
"agingCount": 4,
|
||||
"count": 4,
|
||||
"expiringCommonNameAndDate": "",
|
||||
"expiringCount": 0,
|
||||
"hasAging": true,
|
||||
"hasExpired": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles negative diff values appropriately for aging certs', () => {
|
||||
diffSpy
|
||||
.mockReturnValueOnce(700)
|
||||
.mockReturnValueOnce(-90)
|
||||
.mockReturnValueOnce(-80);
|
||||
const result = getCertSummary(
|
||||
mockCerts,
|
||||
new Date('2020-07-01T12:00:00.000Z').valueOf(),
|
||||
new Date('2019-09-01T03:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 700 days ago.; Common-Three, invalid until 2019-07-22T03:15:39.000Z, 90 days from now.; Common-One, invalid until 2019-07-24T03:15:39.000Z, 80 days from now.",
|
||||
"agingCount": 4,
|
||||
"count": 4,
|
||||
"expiringCommonNameAndDate": "",
|
||||
"expiringCount": 0,
|
||||
"hasAging": true,
|
||||
"hasExpired": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles negative diff values appropriately for expiring certs', () => {
|
||||
diffSpy
|
||||
// negative days are in the future, positive days are in the past
|
||||
.mockReturnValueOnce(-96)
|
||||
.mockReturnValueOnce(-94)
|
||||
.mockReturnValueOnce(2);
|
||||
const result = getCertSummary(
|
||||
mockCerts,
|
||||
new Date('2020-07-20T05:00:00.000Z').valueOf(),
|
||||
new Date('2019-03-01T00:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"agingCommonNameAndDate": "",
|
||||
"agingCount": 0,
|
||||
"count": 4,
|
||||
"expiringCommonNameAndDate": "Common-One, expires on 2020-07-16T03:15:39.000Z in 96 days.; Common-Two, expires on 2020-07-18T03:15:39.000Z in 94 days.; Common-Three, expired on 2020-07-19T03:15:39.000Z 2 days ago.",
|
||||
"expiringCount": 3,
|
||||
"hasAging": null,
|
||||
"hasExpired": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
56
x-pack/plugins/uptime/server/lib/alerts/common.ts
Normal file
56
x-pack/plugins/uptime/server/lib/alerts/common.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { isRight } from 'fp-ts/lib/Either';
|
||||
import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types';
|
||||
|
||||
export type UpdateUptimeAlertState = (
|
||||
state: Record<string, any>,
|
||||
isTriggeredNow: boolean
|
||||
) => UptimeCommonState;
|
||||
|
||||
export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => {
|
||||
const now = new Date().toISOString();
|
||||
const decoded = UptimeCommonStateType.decode(state);
|
||||
if (!isRight(decoded)) {
|
||||
const triggerVal = isTriggeredNow ? now : undefined;
|
||||
return {
|
||||
currentTriggerStarted: triggerVal,
|
||||
firstCheckedAt: now,
|
||||
firstTriggeredAt: triggerVal,
|
||||
isTriggered: isTriggeredNow,
|
||||
lastTriggeredAt: triggerVal,
|
||||
lastCheckedAt: now,
|
||||
lastResolvedAt: undefined,
|
||||
};
|
||||
}
|
||||
const {
|
||||
currentTriggerStarted,
|
||||
firstCheckedAt,
|
||||
firstTriggeredAt,
|
||||
lastTriggeredAt,
|
||||
// this is the stale trigger status, we're naming it `wasTriggered`
|
||||
// to differentiate it from the `isTriggeredNow` param
|
||||
isTriggered: wasTriggered,
|
||||
lastResolvedAt,
|
||||
} = decoded.right;
|
||||
|
||||
let cts: string | undefined;
|
||||
if (isTriggeredNow && !currentTriggerStarted) {
|
||||
cts = now;
|
||||
} else if (isTriggeredNow) {
|
||||
cts = currentTriggerStarted;
|
||||
}
|
||||
return {
|
||||
currentTriggerStarted: cts,
|
||||
firstCheckedAt: firstCheckedAt ?? now,
|
||||
firstTriggeredAt: isTriggeredNow && !firstTriggeredAt ? now : firstTriggeredAt,
|
||||
lastCheckedAt: now,
|
||||
lastTriggeredAt: isTriggeredNow ? now : lastTriggeredAt,
|
||||
lastResolvedAt: !isTriggeredNow && wasTriggered ? now : lastResolvedAt,
|
||||
isTriggered: isTriggeredNow,
|
||||
};
|
||||
};
|
|
@ -6,5 +6,9 @@
|
|||
|
||||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { statusCheckAlertFactory } from './status_check';
|
||||
import { tlsAlertFactory } from './tls';
|
||||
|
||||
export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [statusCheckAlertFactory];
|
||||
export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [
|
||||
statusCheckAlertFactory,
|
||||
tlsAlertFactory,
|
||||
];
|
||||
|
|
|
@ -11,13 +11,11 @@ import { i18n } from '@kbn/i18n';
|
|||
import { AlertExecutorOptions } from '../../../../alerting/server';
|
||||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { GetMonitorStatusResult } from '../requests';
|
||||
import {
|
||||
StatusCheckExecutorParamsType,
|
||||
StatusCheckAlertStateType,
|
||||
StatusCheckAlertState,
|
||||
} from '../../../common/runtime_types';
|
||||
import { StatusCheckExecutorParamsType } from '../../../common/runtime_types';
|
||||
import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants';
|
||||
import { savedObjectsAdapter } from '../saved_objects';
|
||||
import { updateState } from './common';
|
||||
import { commonStateTranslations } from './translations';
|
||||
|
||||
const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS;
|
||||
|
||||
|
@ -34,7 +32,7 @@ export const uniqueMonitorIds = (items: GetMonitorStatusResult[]): Set<string> =
|
|||
/**
|
||||
* Generates a message to include in contexts of alerts.
|
||||
* @param monitors the list of monitors to include in the message
|
||||
* @param max
|
||||
* @param max the maximum number of items the summary should contain
|
||||
*/
|
||||
export const contextMessage = (monitorIds: string[], max: number): string => {
|
||||
const MIN = 2;
|
||||
|
@ -122,58 +120,11 @@ export const fullListByIdAndLocation = (
|
|||
);
|
||||
};
|
||||
|
||||
export const updateState = (
|
||||
state: Record<string, any>,
|
||||
isTriggeredNow: boolean
|
||||
): StatusCheckAlertState => {
|
||||
const now = new Date().toISOString();
|
||||
const decoded = StatusCheckAlertStateType.decode(state);
|
||||
if (!isRight(decoded)) {
|
||||
const triggerVal = isTriggeredNow ? now : undefined;
|
||||
return {
|
||||
currentTriggerStarted: triggerVal,
|
||||
firstCheckedAt: now,
|
||||
firstTriggeredAt: triggerVal,
|
||||
isTriggered: isTriggeredNow,
|
||||
lastTriggeredAt: triggerVal,
|
||||
lastCheckedAt: now,
|
||||
lastResolvedAt: undefined,
|
||||
};
|
||||
}
|
||||
const {
|
||||
currentTriggerStarted,
|
||||
firstCheckedAt,
|
||||
firstTriggeredAt,
|
||||
lastTriggeredAt,
|
||||
// this is the stale trigger status, we're naming it `wasTriggered`
|
||||
// to differentiate it from the `isTriggeredNow` param
|
||||
isTriggered: wasTriggered,
|
||||
lastResolvedAt,
|
||||
} = decoded.right;
|
||||
|
||||
let cts: string | undefined;
|
||||
if (isTriggeredNow && !currentTriggerStarted) {
|
||||
cts = now;
|
||||
} else if (isTriggeredNow) {
|
||||
cts = currentTriggerStarted;
|
||||
}
|
||||
|
||||
return {
|
||||
currentTriggerStarted: cts,
|
||||
firstCheckedAt: firstCheckedAt ?? now,
|
||||
firstTriggeredAt: isTriggeredNow && !firstTriggeredAt ? now : firstTriggeredAt,
|
||||
lastCheckedAt: now,
|
||||
lastTriggeredAt: isTriggeredNow ? now : lastTriggeredAt,
|
||||
lastResolvedAt: !isTriggeredNow && wasTriggered ? now : lastResolvedAt,
|
||||
isTriggered: isTriggeredNow,
|
||||
};
|
||||
};
|
||||
|
||||
// Right now the maximum number of monitors shown in the message is hardcoded here.
|
||||
// we might want to make this a parameter in the future
|
||||
const DEFAULT_MAX_MESSAGE_ROWS = 3;
|
||||
|
||||
export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) => ({
|
||||
export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
|
||||
id: 'xpack.uptime.alerts.monitorStatus',
|
||||
name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
|
||||
defaultMessage: 'Uptime monitor status',
|
||||
|
@ -218,72 +169,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) =>
|
|||
),
|
||||
},
|
||||
],
|
||||
state: [
|
||||
{
|
||||
name: 'firstCheckedAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstCheckedAt',
|
||||
{
|
||||
defaultMessage: 'Timestamp indicating when this alert first checked',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'firstTriggeredAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstTriggeredAt',
|
||||
{
|
||||
defaultMessage: 'Timestamp indicating when the alert first triggered',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'currentTriggerStarted',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.currentTriggerStarted',
|
||||
{
|
||||
defaultMessage:
|
||||
'Timestamp indicating when the current trigger state began, if alert is triggered',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'isTriggered',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.isTriggered',
|
||||
{
|
||||
defaultMessage: `Flag indicating if the alert is currently triggering`,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'lastCheckedAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastCheckedAt',
|
||||
{
|
||||
defaultMessage: `Timestamp indicating the alert's most recent check time`,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'lastResolvedAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastResolvedAt',
|
||||
{
|
||||
defaultMessage: `Timestamp indicating the most recent resolution time for this alert`,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'lastTriggeredAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastTriggeredAt',
|
||||
{
|
||||
defaultMessage: `Timestamp indicating the alert's most recent trigger time`,
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
state: [...commonStateTranslations],
|
||||
},
|
||||
async executor(options: AlertExecutorOptions) {
|
||||
const { params: rawParams } = options;
|
||||
|
|
152
x-pack/plugins/uptime/server/lib/alerts/tls.ts
Normal file
152
x-pack/plugins/uptime/server/lib/alerts/tls.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 { UptimeAlertTypeFactory } from './types';
|
||||
import { savedObjectsAdapter } from '../saved_objects';
|
||||
import { updateState } from './common';
|
||||
import { ACTION_GROUP_DEFINITIONS, DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
|
||||
import { Cert, CertResult } from '../../../common/runtime_types';
|
||||
import { commonStateTranslations, tlsTranslations } from './translations';
|
||||
|
||||
const { TLS } = ACTION_GROUP_DEFINITIONS;
|
||||
|
||||
const DEFAULT_FROM = 'now-1d';
|
||||
const DEFAULT_TO = 'now';
|
||||
const DEFAULT_INDEX = 0;
|
||||
const DEFAULT_SIZE = 20;
|
||||
|
||||
interface TlsAlertState {
|
||||
count: number;
|
||||
agingCount: number;
|
||||
agingCommonNameAndDate: string;
|
||||
expiringCount: number;
|
||||
expiringCommonNameAndDate: string;
|
||||
hasAging: true | null;
|
||||
hasExpired: true | null;
|
||||
}
|
||||
|
||||
const sortCerts = (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf();
|
||||
|
||||
const mapCertsToSummaryString = (
|
||||
certs: Cert[],
|
||||
certLimitMessage: (cert: Cert) => string,
|
||||
maxSummaryItems: number
|
||||
): string =>
|
||||
certs
|
||||
.slice(0, maxSummaryItems)
|
||||
.map(cert => `${cert.common_name}, ${certLimitMessage(cert)}`)
|
||||
.reduce((prev, cur) => (prev === '' ? cur : prev.concat(`; ${cur}`)), '');
|
||||
|
||||
const getValidAfter = ({ not_after: date }: Cert) => {
|
||||
if (!date) return 'Error, missing `certificate_not_valid_after` date.';
|
||||
const relativeDate = moment().diff(date, 'days');
|
||||
return relativeDate >= 0
|
||||
? tlsTranslations.validAfterExpiredString(date, relativeDate)
|
||||
: tlsTranslations.validAfterExpiringString(date, Math.abs(relativeDate));
|
||||
};
|
||||
|
||||
const getValidBefore = ({ not_before: date }: Cert): string => {
|
||||
if (!date) return 'Error, missing `certificate_not_valid_before` date.';
|
||||
const relativeDate = moment().diff(date, 'days');
|
||||
return relativeDate >= 0
|
||||
? tlsTranslations.validBeforeExpiredString(date, relativeDate)
|
||||
: tlsTranslations.validBeforeExpiringString(date, Math.abs(relativeDate));
|
||||
};
|
||||
|
||||
export const getCertSummary = (
|
||||
certs: Cert[],
|
||||
expirationThreshold: number,
|
||||
ageThreshold: number,
|
||||
maxSummaryItems: number = 3
|
||||
): TlsAlertState => {
|
||||
certs.sort((a, b) => sortCerts(a.not_after ?? '', b.not_after ?? ''));
|
||||
const expiring = certs.filter(
|
||||
cert => new Date(cert.not_after ?? '').valueOf() < expirationThreshold
|
||||
);
|
||||
|
||||
certs.sort((a, b) => sortCerts(a.not_before ?? '', b.not_before ?? ''));
|
||||
const aging = certs.filter(cert => new Date(cert.not_before ?? '').valueOf() < ageThreshold);
|
||||
|
||||
return {
|
||||
count: certs.length,
|
||||
agingCount: aging.length,
|
||||
agingCommonNameAndDate: mapCertsToSummaryString(aging, getValidBefore, maxSummaryItems),
|
||||
expiringCommonNameAndDate: mapCertsToSummaryString(expiring, getValidAfter, maxSummaryItems),
|
||||
expiringCount: expiring.length,
|
||||
hasAging: aging.length > 0 ? true : null,
|
||||
hasExpired: expiring.length > 0 ? true : null,
|
||||
};
|
||||
};
|
||||
|
||||
export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
|
||||
id: 'xpack.uptime.alerts.tls',
|
||||
name: tlsTranslations.alertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({}),
|
||||
},
|
||||
defaultActionGroupId: TLS.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: TLS.id,
|
||||
name: TLS.name,
|
||||
},
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
async executor(options) {
|
||||
const {
|
||||
services: { alertInstanceFactory, callCluster, savedObjectsClient },
|
||||
state,
|
||||
} = options;
|
||||
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
|
||||
|
||||
const { certs, total }: CertResult = await libs.requests.getCerts({
|
||||
callES: callCluster,
|
||||
dynamicSettings,
|
||||
from: DEFAULT_FROM,
|
||||
to: DEFAULT_TO,
|
||||
index: DEFAULT_INDEX,
|
||||
size: DEFAULT_SIZE,
|
||||
notValidAfter: `now+${dynamicSettings.certThresholds?.expiration ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.expiration}d`,
|
||||
notValidBefore: `now-${dynamicSettings.certThresholds?.age ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.age}d`,
|
||||
sortBy: 'common_name',
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const foundCerts = total > 0;
|
||||
|
||||
if (foundCerts) {
|
||||
const absoluteExpirationThreshold = moment()
|
||||
.add(
|
||||
dynamicSettings.certThresholds?.expiration ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.expiration,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const absoluteAgeThreshold = moment()
|
||||
.subtract(
|
||||
dynamicSettings.certThresholds?.age ?? DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.age,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const alertInstance = alertInstanceFactory(TLS.id);
|
||||
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, foundCerts),
|
||||
...summary,
|
||||
});
|
||||
alertInstance.scheduleActions(TLS.id);
|
||||
}
|
||||
|
||||
return updateState(state, foundCerts);
|
||||
},
|
||||
});
|
150
x-pack/plugins/uptime/server/lib/alerts/translations.ts
Normal file
150
x-pack/plugins/uptime/server/lib/alerts/translations.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 commonStateTranslations = [
|
||||
{
|
||||
name: 'firstCheckedAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstCheckedAt',
|
||||
{
|
||||
defaultMessage: 'Timestamp indicating when this alert first checked',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'firstTriggeredAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstTriggeredAt',
|
||||
{
|
||||
defaultMessage: 'Timestamp indicating when the alert first triggered',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'currentTriggerStarted',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.currentTriggerStarted',
|
||||
{
|
||||
defaultMessage:
|
||||
'Timestamp indicating when the current trigger state began, if alert is triggered',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'isTriggered',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.isTriggered',
|
||||
{
|
||||
defaultMessage: `Flag indicating if the alert is currently triggering`,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'lastCheckedAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastCheckedAt',
|
||||
{
|
||||
defaultMessage: `Timestamp indicating the alert's most recent check time`,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'lastResolvedAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastResolvedAt',
|
||||
{
|
||||
defaultMessage: `Timestamp indicating the most recent resolution time for this alert`,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'lastTriggeredAt',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastTriggeredAt',
|
||||
{
|
||||
defaultMessage: `Timestamp indicating the alert's most recent trigger time`,
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const tlsTranslations = {
|
||||
alertFactoryName: i18n.translate('xpack.uptime.alerts.tls', {
|
||||
defaultMessage: 'Uptime TLS',
|
||||
}),
|
||||
actionVariables: [
|
||||
{
|
||||
name: 'count',
|
||||
description: i18n.translate('xpack.uptime.alerts.tls.actionVariables.state.count', {
|
||||
defaultMessage: 'The number of certs detected by the alert executor',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'expiringCount',
|
||||
description: i18n.translate('xpack.uptime.alerts.tls.actionVariables.state.expiringCount', {
|
||||
defaultMessage: 'The number of expiring certs detected by the alert.',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'expiringCommonNameAndDate',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.tls.actionVariables.state.expiringCommonNameAndDate',
|
||||
{
|
||||
defaultMessage: 'The common names and expiration date/time of the detected certs',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'agingCount',
|
||||
description: i18n.translate('xpack.uptime.alerts.tls.actionVariables.state.agingCount', {
|
||||
defaultMessage: 'The number of detected certs that are becoming too old.',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'agingCommonNameAndDate',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.tls.actionVariables.state.agingCommonNameAndDate',
|
||||
{
|
||||
defaultMessage: 'The common names and expiration date/time of the detected certs.',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
validAfterExpiredString: (date: string, relativeDate: number) =>
|
||||
i18n.translate('xpack.uptime.alerts.tls.validAfterExpiredString', {
|
||||
defaultMessage: `expired on {date} {relativeDate} days ago.`,
|
||||
values: {
|
||||
date,
|
||||
relativeDate,
|
||||
},
|
||||
}),
|
||||
validAfterExpiringString: (date: string, relativeDate: number) =>
|
||||
i18n.translate('xpack.uptime.alerts.tls.validAfterExpiringString', {
|
||||
defaultMessage: `expires on {date} in {relativeDate} days.`,
|
||||
values: {
|
||||
date,
|
||||
relativeDate,
|
||||
},
|
||||
}),
|
||||
validBeforeExpiredString: (date: string, relativeDate: number) =>
|
||||
i18n.translate('xpack.uptime.alerts.tls.validBeforeExpiredString', {
|
||||
defaultMessage: 'valid since {date}, {relativeDate} days ago.',
|
||||
values: {
|
||||
date,
|
||||
relativeDate,
|
||||
},
|
||||
}),
|
||||
validBeforeExpiringString: (date: string, relativeDate: number) =>
|
||||
i18n.translate('xpack.uptime.alerts.tls.validBeforeExpiringString', {
|
||||
defaultMessage: 'invalid until {date}, {relativeDate} days from now.',
|
||||
values: {
|
||||
date,
|
||||
relativeDate,
|
||||
},
|
||||
}),
|
||||
};
|
|
@ -20,8 +20,10 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn
|
|||
index,
|
||||
from,
|
||||
to,
|
||||
search,
|
||||
size,
|
||||
search,
|
||||
notValidBefore,
|
||||
notValidAfter,
|
||||
sortBy,
|
||||
direction,
|
||||
}) => {
|
||||
|
@ -91,6 +93,10 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn
|
|||
},
|
||||
};
|
||||
|
||||
if (!params.body.query.bool.should) {
|
||||
params.body.query.bool.should = [];
|
||||
}
|
||||
|
||||
if (search) {
|
||||
params.body.query.bool.minimum_should_match = 1;
|
||||
params.body.query.bool.should = [
|
||||
|
@ -110,6 +116,35 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn
|
|||
];
|
||||
}
|
||||
|
||||
if (notValidBefore || notValidAfter) {
|
||||
const validityFilters: any = {
|
||||
bool: {
|
||||
should: [],
|
||||
},
|
||||
};
|
||||
if (notValidBefore) {
|
||||
validityFilters.bool.should.push({
|
||||
range: {
|
||||
'tls.certificate_not_valid_before': {
|
||||
lte: notValidBefore,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (notValidAfter) {
|
||||
validityFilters.bool.should.push({
|
||||
range: {
|
||||
'tls.certificate_not_valid_after': {
|
||||
lte: notValidAfter,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
params.body.query.bool.filter.push(validityFilters);
|
||||
}
|
||||
|
||||
// console.log(JSON.stringify(params, null, 2));
|
||||
const result = await callES('search', params);
|
||||
|
||||
const certs = (result?.hits?.hits ?? []).map((hit: any) => {
|
||||
|
|
|
@ -46,6 +46,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./monitor_states_generated'));
|
||||
loadTestFile(require.resolve('./telemetry_collectors'));
|
||||
});
|
||||
|
||||
describe('with real-world data', () => {
|
||||
beforeEach('load heartbeat data', async () => await esArchiver.load('uptime/full_heartbeat'));
|
||||
afterEach('unload', async () => await esArchiver.unload('uptime/full_heartbeat'));
|
||||
|
|
|
@ -13,6 +13,7 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) {
|
|||
return {
|
||||
async openFlyout() {
|
||||
await testSubjects.click('xpack.uptime.alertsPopover.toggleButton', 5000);
|
||||
await testSubjects.click('xpack.uptime.openAlertContextPanel', 5000);
|
||||
await testSubjects.click('xpack.uptime.toggleAlertFlyout', 5000);
|
||||
},
|
||||
async openMonitorStatusAlertType(alertType: string) {
|
||||
|
|
Loading…
Reference in a new issue