Revert "[Monitoring] Cluster state watch to Kibana alerting (#61685)"

This reverts commit ab0cc8894a.
This commit is contained in:
spalger 2020-04-06 17:05:09 -07:00
parent 29c1aad2bb
commit f1bd3bdacb
30 changed files with 772 additions and 1586 deletions

View file

@ -239,15 +239,11 @@ export const ALERT_TYPE_PREFIX = 'monitoring_';
* This is the alert type id for the license expiration alert
*/
export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`;
/**
* This is the alert type id for the cluster state alert
*/
export const ALERT_TYPE_CLUSTER_STATE = `${ALERT_TYPE_PREFIX}alert_type_cluster_state`;
/**
* A listing of all alert types
*/
export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_STATE];
export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION];
/**
* Matches the id for the built-in in email action type
@ -258,7 +254,7 @@ export const ALERT_ACTION_TYPE_EMAIL = '.email';
/**
* The number of alerts that have been migrated
*/
export const NUMBER_OF_MIGRATED_ALERTS = 2;
export const NUMBER_OF_MIGRATED_ALERTS = 1;
/**
* The advanced settings config name for the email address

View file

@ -6,15 +6,10 @@
import React from 'react';
import chrome from '../../np_imports/ui/chrome';
import { capitalize, get } from 'lodash';
import { capitalize } from 'lodash';
import { formatDateTimeLocal } from '../../../common/formatting';
import { formatTimestampToDuration } from '../../../common';
import {
CALCULATE_DURATION_SINCE,
EUI_SORT_DESCENDING,
ALERT_TYPE_LICENSE_EXPIRATION,
ALERT_TYPE_CLUSTER_STATE,
} from '../../../common/constants';
import { CALCULATE_DURATION_SINCE, EUI_SORT_DESCENDING } from '../../../common/constants';
import { mapSeverity } from './map_severity';
import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert';
import { EuiMonitoringTable } from 'plugins/monitoring/components/table';
@ -26,8 +21,6 @@ const linkToCategories = {
'elasticsearch/indices': 'Elasticsearch Indices',
'kibana/instances': 'Kibana Instances',
'logstash/instances': 'Logstash Nodes',
[ALERT_TYPE_LICENSE_EXPIRATION]: 'License expiration',
[ALERT_TYPE_CLUSTER_STATE]: 'Cluster state',
};
const getColumns = (kbnUrl, scope, timezone) => [
{
@ -101,22 +94,19 @@ const getColumns = (kbnUrl, scope, timezone) => [
}),
field: 'message',
sortable: true,
render: (_message, alert) => {
const message = get(alert, 'message.text', get(alert, 'message', ''));
return (
<FormattedAlert
prefix={alert.prefix}
suffix={alert.suffix}
message={message}
metadata={alert.metadata}
changeUrl={target => {
scope.$evalAsync(() => {
kbnUrl.changePath(target);
});
}}
/>
);
},
render: (message, alert) => (
<FormattedAlert
prefix={alert.prefix}
suffix={alert.suffix}
message={message}
metadata={alert.metadata}
changeUrl={target => {
scope.$evalAsync(() => {
kbnUrl.changePath(target);
});
}}
/>
),
},
{
name: i18n.translate('xpack.monitoring.alerts.categoryColumnTitle', {
@ -158,8 +148,8 @@ const getColumns = (kbnUrl, scope, timezone) => [
export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) => {
const alertsFlattened = alerts.map(alert => ({
...alert,
status: get(alert, 'metadata.severity', get(alert, 'severity', 0)),
category: get(alert, 'metadata.link', get(alert, 'type', null)),
status: alert.metadata.severity,
category: alert.metadata.link,
}));
const injector = chrome.dangerouslyGetActiveInjector();

View file

@ -8,7 +8,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { kfetch } from 'ui/kfetch';
import { AlertsStatus, AlertsStatusProps } from './status';
import { ALERT_TYPES } from '../../../common/constants';
import { ALERT_TYPE_PREFIX } from '../../../common/constants';
import { getSetupModeState } from '../../lib/setup_mode';
import { mockUseEffects } from '../../jest.helpers';
@ -63,7 +63,11 @@ describe('Status', () => {
it('should render a success message if all alerts have been migrated and in setup mode', async () => {
(kfetch as jest.Mock).mockReturnValue({
data: ALERT_TYPES.map(type => ({ alertTypeId: type })),
data: [
{
alertTypeId: ALERT_TYPE_PREFIX,
},
],
});
(getSetupModeState as jest.Mock).mockReturnValue({

View file

@ -142,7 +142,7 @@ export const AlertsStatus: React.FC<AlertsStatusProps> = (props: AlertsStatusPro
);
}
const allMigrated = kibanaAlerts.length >= NUMBER_OF_MIGRATED_ALERTS;
const allMigrated = kibanaAlerts.length === NUMBER_OF_MIGRATED_ALERTS;
if (allMigrated) {
if (setupModeEnabled) {
return (

View file

@ -6,12 +6,14 @@
import React, { Fragment } from 'react';
import moment from 'moment-timezone';
import chrome from '../../../np_imports/ui/chrome';
import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert';
import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity';
import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration';
import {
CALCULATE_DURATION_SINCE,
KIBANA_ALERTING_ENABLED,
ALERT_TYPE_LICENSE_EXPIRATION,
CALCULATE_DURATION_UNTIL,
} from '../../../../common/constants';
import { formatDateTimeLocal } from '../../../../common/formatting';
@ -29,37 +31,6 @@ import {
EuiLink,
} from '@elastic/eui';
function replaceTokens(alert) {
if (!alert.message.tokens) {
return alert.message.text;
}
let text = alert.message.text;
for (const token of alert.message.tokens) {
if (token.type === 'time') {
text = text.replace(
token.startToken,
token.isRelative
? formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL)
: moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z')
);
} else if (token.type === 'link') {
const linkPart = new RegExp(`${token.startToken}(.+?)${token.endToken}`).exec(text);
// TODO: we assume this is at the end, which works for now but will not always work
const nonLinkText = text.replace(linkPart[0], '');
text = (
<Fragment>
{nonLinkText}
<EuiLink href={`#${token.url}`}>{linkPart[1]}</EuiLink>
</Fragment>
);
}
}
return text;
}
export function AlertsPanel({ alerts, changeUrl }) {
const goToAlerts = () => changeUrl('/alerts');
@ -87,6 +58,9 @@ export function AlertsPanel({ alerts, changeUrl }) {
severityIcon.iconType = 'check';
}
const injector = chrome.dangerouslyGetActiveInjector();
const timezone = injector.get('config').get('dateFormat:tz');
return (
<EuiCallOut
key={`alert-item-${index}`}
@ -109,7 +83,7 @@ export function AlertsPanel({ alerts, changeUrl }) {
id="xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText"
defaultMessage="Last checked {updateDateTime} (triggered {duration} ago)"
values={{
updateDateTime: formatDateTimeLocal(item.update_timestamp),
updateDateTime: formatDateTimeLocal(item.update_timestamp, timezone),
duration: formatTimestampToDuration(item.timestamp, CALCULATE_DURATION_SINCE),
}}
/>
@ -122,7 +96,14 @@ export function AlertsPanel({ alerts, changeUrl }) {
const alertsList = KIBANA_ALERTING_ENABLED
? alerts.map((alert, idx) => {
const callOutProps = mapSeverity(alert.severity);
const message = replaceTokens(alert);
let message = alert.message
// scan message prefix and replace relative times
// \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_].
.replace(
'#relative',
formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL)
)
.replace('#absolute', moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z'));
if (!alert.isFiring) {
callOutProps.title = i18n.translate(
@ -137,30 +118,22 @@ export function AlertsPanel({ alerts, changeUrl }) {
);
callOutProps.color = 'success';
callOutProps.iconType = 'check';
} else {
if (alert.type === ALERT_TYPE_LICENSE_EXPIRATION) {
message = (
<Fragment>
{message}
&nbsp;
<EuiLink href="#license">Please update your license</EuiLink>
</Fragment>
);
}
}
return (
<Fragment key={idx}>
<EuiCallOut {...callOutProps}>
<p>{message}</p>
<EuiText size="xs">
<p data-test-subj="alertMeta" className="monCallout--meta">
<FormattedMessage
id="xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText"
defaultMessage="Last checked {updateDateTime} (triggered {duration} ago)"
values={{
updateDateTime: formatDateTimeLocal(alert.lastCheckedMS),
duration: formatTimestampToDuration(
alert.triggeredMS,
CALCULATE_DURATION_SINCE
),
}}
/>
</p>
</EuiText>
</EuiCallOut>
<EuiSpacer />
</Fragment>
<EuiCallOut key={idx} {...callOutProps}>
<p>{message}</p>
</EuiCallOut>
);
})
: alerts.map((item, index) => (

View file

@ -18,37 +18,25 @@ import { Alerts } from '../../components/alerts';
import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui';
import { CODE_PATH_ALERTS, KIBANA_ALERTING_ENABLED } from '../../../common/constants';
import { CODE_PATH_ALERTS } from '../../../common/constants';
function getPageData($injector) {
const globalState = $injector.get('globalState');
const $http = $injector.get('$http');
const Private = $injector.get('Private');
const url = KIBANA_ALERTING_ENABLED
? `../api/monitoring/v1/alert_status`
: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`;
const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`;
const timeBounds = timefilter.getBounds();
const data = {
timeRange: {
min: timeBounds.min.toISOString(),
max: timeBounds.max.toISOString(),
},
};
if (!KIBANA_ALERTING_ENABLED) {
data.ccs = globalState.ccs;
}
return $http
.post(url, data)
.then(response => {
const result = get(response, 'data', []);
if (KIBANA_ALERTING_ENABLED) {
return result.alerts;
}
return result;
.post(url, {
ccs: globalState.ccs,
timeRange: {
min: timeBounds.min.toISOString(),
max: timeBounds.max.toISOString(),
},
})
.then(response => get(response, 'data', []))
.catch(err => {
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);

View file

@ -239,15 +239,11 @@ export const ALERT_TYPE_PREFIX = 'monitoring_';
* This is the alert type id for the license expiration alert
*/
export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`;
/**
* This is the alert type id for the cluster state alert
*/
export const ALERT_TYPE_CLUSTER_STATE = `${ALERT_TYPE_PREFIX}alert_type_cluster_state`;
/**
* A listing of all alert types
*/
export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_STATE];
export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION];
/**
* Matches the id for the built-in in email action type
@ -258,7 +254,7 @@ export const ALERT_ACTION_TYPE_EMAIL = '.email';
/**
* The number of alerts that have been migrated
*/
export const NUMBER_OF_MIGRATED_ALERTS = 2;
export const NUMBER_OF_MIGRATED_ALERTS = 1;
/**
* The advanced settings config name for the email address

View file

@ -1,186 +0,0 @@
/*
* 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 { Logger } from 'src/core/server';
import { savedObjectsClientMock } from 'src/core/server/mocks';
import { getClusterState } from './cluster_state';
import { AlertServices } from '../../../alerting/server';
import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants';
import { AlertCommonParams, AlertCommonState, AlertClusterStatePerClusterState } from './types';
import { getPreparedAlert } from '../lib/alerts/get_prepared_alert';
import { executeActions } from '../lib/alerts/cluster_state.lib';
import { AlertClusterStateState } from './enums';
jest.mock('../lib/alerts/cluster_state.lib', () => ({
executeActions: jest.fn(),
getUiMessage: jest.fn(),
}));
jest.mock('../lib/alerts/get_prepared_alert', () => ({
getPreparedAlert: jest.fn(() => {
return {
emailAddress: 'foo@foo.com',
};
}),
}));
interface MockServices {
callCluster: jest.Mock;
alertInstanceFactory: jest.Mock;
savedObjectsClient: jest.Mock;
}
describe('getClusterState', () => {
const services: MockServices | AlertServices = {
callCluster: jest.fn(),
alertInstanceFactory: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
};
const params: AlertCommonParams = {
dateFormat: 'YYYY',
timezone: 'UTC',
};
const emailAddress = 'foo@foo.com';
const clusterUuid = 'kdksdfj434';
const clusterName = 'monitoring_test';
const cluster = { clusterUuid, clusterName };
async function setupAlert(
previousState: AlertClusterStateState,
newState: AlertClusterStateState
): Promise<AlertCommonState> {
const logger: Logger = {
warn: jest.fn(),
log: jest.fn(),
debug: jest.fn(),
trace: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
info: jest.fn(),
get: jest.fn(),
};
const getLogger = (): Logger => logger;
const ccrEnabled = false;
(getPreparedAlert as jest.Mock).mockImplementation(() => ({
emailAddress,
data: [
{
state: newState,
clusterUuid,
},
],
clusters: [cluster],
}));
const alert = getClusterState(null as any, null as any, getLogger, ccrEnabled);
const state: AlertCommonState = {
[clusterUuid]: {
state: previousState,
ui: {
isFiring: false,
severity: 0,
message: null,
resolvedMS: 0,
lastCheckedMS: 0,
triggeredMS: 0,
},
} as AlertClusterStatePerClusterState,
};
return (await alert.executor({ services, params, state } as any)) as AlertCommonState;
}
afterEach(() => {
(executeActions as jest.Mock).mockClear();
});
it('should configure the alert properly', () => {
const alert = getClusterState(null as any, null as any, jest.fn(), false);
expect(alert.id).toBe(ALERT_TYPE_CLUSTER_STATE);
expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]);
});
it('should alert if green -> yellow', async () => {
const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Yellow);
expect(executeActions).toHaveBeenCalledWith(
undefined,
cluster,
AlertClusterStateState.Yellow,
emailAddress
);
const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState;
expect(clusterResult.state).toBe(AlertClusterStateState.Yellow);
expect(clusterResult.ui.isFiring).toBe(true);
expect(clusterResult.ui.resolvedMS).toBe(0);
});
it('should alert if yellow -> green', async () => {
const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Green);
expect(executeActions).toHaveBeenCalledWith(
undefined,
cluster,
AlertClusterStateState.Green,
emailAddress,
true
);
const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState;
expect(clusterResult.state).toBe(AlertClusterStateState.Green);
expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0);
});
it('should alert if green -> red', async () => {
const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Red);
expect(executeActions).toHaveBeenCalledWith(
undefined,
cluster,
AlertClusterStateState.Red,
emailAddress
);
const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState;
expect(clusterResult.state).toBe(AlertClusterStateState.Red);
expect(clusterResult.ui.isFiring).toBe(true);
expect(clusterResult.ui.resolvedMS).toBe(0);
});
it('should alert if red -> green', async () => {
const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Green);
expect(executeActions).toHaveBeenCalledWith(
undefined,
cluster,
AlertClusterStateState.Green,
emailAddress,
true
);
const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState;
expect(clusterResult.state).toBe(AlertClusterStateState.Green);
expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0);
});
it('should not alert if red -> yellow', async () => {
const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Yellow);
expect(executeActions).not.toHaveBeenCalled();
const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState;
expect(clusterResult.state).toBe(AlertClusterStateState.Red);
expect(clusterResult.ui.resolvedMS).toBe(0);
});
it('should not alert if yellow -> red', async () => {
const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Red);
expect(executeActions).not.toHaveBeenCalled();
const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState;
expect(clusterResult.state).toBe(AlertClusterStateState.Yellow);
expect(clusterResult.ui.resolvedMS).toBe(0);
});
it('should not alert if green -> green', async () => {
const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Green);
expect(executeActions).not.toHaveBeenCalled();
const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState;
expect(clusterResult.state).toBe(AlertClusterStateState.Green);
expect(clusterResult.ui.resolvedMS).toBe(0);
});
});

View file

@ -1,134 +0,0 @@
/*
* 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-timezone';
import { i18n } from '@kbn/i18n';
import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server';
import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants';
import { AlertType } from '../../../alerting/server';
import { executeActions, getUiMessage } from '../lib/alerts/cluster_state.lib';
import {
AlertCommonExecutorOptions,
AlertCommonState,
AlertClusterStatePerClusterState,
AlertCommonCluster,
} from './types';
import { AlertClusterStateState } from './enums';
import { getPreparedAlert } from '../lib/alerts/get_prepared_alert';
import { fetchClusterState } from '../lib/alerts/fetch_cluster_state';
export const getClusterState = (
getUiSettingsService: () => Promise<UiSettingsServiceStart>,
monitoringCluster: ICustomClusterClient,
getLogger: (...scopes: string[]) => Logger,
ccsEnabled: boolean
): AlertType => {
const logger = getLogger(ALERT_TYPE_CLUSTER_STATE);
return {
id: ALERT_TYPE_CLUSTER_STATE,
name: 'Monitoring Alert - Cluster Status',
actionGroups: [
{
id: 'default',
name: i18n.translate('xpack.monitoring.alerts.clusterState.actionGroups.default', {
defaultMessage: 'Default',
}),
},
],
defaultActionGroupId: 'default',
async executor({
services,
params,
state,
}: AlertCommonExecutorOptions): Promise<AlertCommonState> {
logger.debug(
`Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}`
);
const preparedAlert = await getPreparedAlert(
ALERT_TYPE_CLUSTER_STATE,
getUiSettingsService,
monitoringCluster,
logger,
ccsEnabled,
services,
fetchClusterState
);
if (!preparedAlert) {
return state;
}
const { emailAddress, data: states, clusters } = preparedAlert;
const result: AlertCommonState = { ...state };
const defaultAlertState: AlertClusterStatePerClusterState = {
state: AlertClusterStateState.Green,
ui: {
isFiring: false,
message: null,
severity: 0,
resolvedMS: 0,
triggeredMS: 0,
lastCheckedMS: 0,
},
};
for (const clusterState of states) {
const alertState: AlertClusterStatePerClusterState =
(state[clusterState.clusterUuid] as AlertClusterStatePerClusterState) ||
defaultAlertState;
const cluster = clusters.find(
(c: AlertCommonCluster) => c.clusterUuid === clusterState.clusterUuid
);
if (!cluster) {
logger.warn(`Unable to find cluster for clusterUuid='${clusterState.clusterUuid}'`);
continue;
}
const isNonGreen = clusterState.state !== AlertClusterStateState.Green;
const severity = clusterState.state === AlertClusterStateState.Red ? 2100 : 1100;
const ui = alertState.ui;
let triggered = ui.triggeredMS;
let resolved = ui.resolvedMS;
let message = ui.message || {};
let lastState = alertState.state;
const instance = services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE);
if (isNonGreen) {
if (lastState === AlertClusterStateState.Green) {
logger.debug(`Cluster state changed from green to ${clusterState.state}`);
executeActions(instance, cluster, clusterState.state, emailAddress);
lastState = clusterState.state;
triggered = moment().valueOf();
}
message = getUiMessage(clusterState.state);
resolved = 0;
} else if (!isNonGreen && lastState !== AlertClusterStateState.Green) {
logger.debug(`Cluster state changed from ${lastState} to green`);
executeActions(instance, cluster, clusterState.state, emailAddress, true);
lastState = clusterState.state;
message = getUiMessage(clusterState.state, true);
resolved = moment().valueOf();
}
result[clusterState.clusterUuid] = {
state: lastState,
ui: {
message,
isFiring: isNonGreen,
severity,
resolvedMS: resolved,
triggeredMS: triggered,
lastCheckedMS: moment().valueOf(),
},
} as AlertClusterStatePerClusterState;
}
return result;
},
};
};

View file

@ -1,16 +0,0 @@
/*
* 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.
*/
export enum AlertClusterStateState {
Green = 'green',
Red = 'red',
Yellow = 'yellow',
}
export enum AlertCommonPerClusterMessageTokenType {
Time = 'time',
Link = 'link',
}

View file

@ -6,31 +6,42 @@
import moment from 'moment-timezone';
import { getLicenseExpiration } from './license_expiration';
import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants';
import {
ALERT_TYPE_LICENSE_EXPIRATION,
MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS,
} from '../../common/constants';
import { Logger } from 'src/core/server';
import { AlertServices } from '../../../alerting/server';
import { AlertServices, AlertInstance } from '../../../alerting/server';
import { savedObjectsClientMock } from 'src/core/server/mocks';
import {
AlertCommonParams,
AlertCommonState,
AlertLicensePerClusterState,
AlertLicense,
AlertState,
AlertClusterState,
AlertParams,
LicenseExpirationAlertExecutorOptions,
} from './types';
import { executeActions } from '../lib/alerts/license_expiration.lib';
import { PreparedAlert, getPreparedAlert } from '../lib/alerts/get_prepared_alert';
import { SavedObject, SavedObjectAttributes } from 'src/core/server';
import { SavedObjectsClientContract } from 'src/core/server';
jest.mock('../lib/alerts/license_expiration.lib', () => ({
executeActions: jest.fn(),
getUiMessage: jest.fn(),
}));
function fillLicense(license: any, clusterUuid?: string) {
return {
hits: {
hits: [
{
_source: {
license,
cluster_uuid: clusterUuid,
},
},
],
},
};
}
jest.mock('../lib/alerts/get_prepared_alert', () => ({
getPreparedAlert: jest.fn(() => {
return {
emailAddress: 'foo@foo.com',
};
}),
}));
const clusterUuid = 'a4545jhjb';
const params: AlertParams = {
dateFormat: 'YYYY',
timezone: 'UTC',
};
interface MockServices {
callCluster: jest.Mock;
@ -38,169 +49,428 @@ interface MockServices {
savedObjectsClient: jest.Mock;
}
const alertExecutorOptions: LicenseExpirationAlertExecutorOptions = {
alertId: '',
startedAt: new Date(),
services: {
callCluster: (path: string, opts: any) => new Promise(resolve => resolve()),
alertInstanceFactory: (id: string) => new AlertInstance(),
savedObjectsClient: {} as jest.Mocked<SavedObjectsClientContract>,
},
params: {},
state: {},
spaceId: '',
name: '',
tags: [],
previousStartedAt: null,
createdBy: null,
updatedBy: null,
};
describe('getLicenseExpiration', () => {
const services: MockServices | AlertServices = {
callCluster: jest.fn(),
alertInstanceFactory: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
};
const params: AlertCommonParams = {
dateFormat: 'YYYY',
timezone: 'UTC',
};
const emailAddress = 'foo@foo.com';
const clusterUuid = 'kdksdfj434';
const clusterName = 'monitoring_test';
const dateFormat = 'YYYY-MM-DD';
const cluster = { clusterUuid, clusterName };
const defaultUiState = {
isFiring: false,
severity: 0,
message: null,
resolvedMS: 0,
lastCheckedMS: 0,
triggeredMS: 0,
const getUiSettingsService: any = () => ({
asScopedToClient: (): any => ({
get: () => new Promise(resolve => resolve(emailAddress)),
}),
});
const monitoringCluster: any = null;
const logger: Logger = {
warn: jest.fn(),
log: jest.fn(),
debug: jest.fn(),
trace: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
info: jest.fn(),
get: jest.fn(),
};
async function setupAlert(
license: AlertLicense | null,
expiredCheckDateMS: number,
preparedAlertResponse: PreparedAlert | null | undefined = undefined
): Promise<AlertCommonState> {
const logger: Logger = {
warn: jest.fn(),
log: jest.fn(),
debug: jest.fn(),
trace: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
info: jest.fn(),
get: jest.fn(),
};
const getLogger = (): Logger => logger;
const ccrEnabled = false;
(getPreparedAlert as jest.Mock).mockImplementation(() => {
if (preparedAlertResponse !== undefined) {
return preparedAlertResponse;
}
return {
emailAddress,
data: [license],
clusters: [cluster],
dateFormat,
};
});
const alert = getLicenseExpiration(null as any, null as any, getLogger, ccrEnabled);
const state: AlertCommonState = {
[clusterUuid]: {
expiredCheckDateMS,
ui: { ...defaultUiState },
} as AlertLicensePerClusterState,
};
return (await alert.executor({ services, params, state } as any)) as AlertCommonState;
}
const getLogger = (): Logger => logger;
const ccrEnabled = false;
afterEach(() => {
(executeActions as jest.Mock).mockClear();
(getPreparedAlert as jest.Mock).mockClear();
(logger.warn as jest.Mock).mockClear();
});
it('should have the right id and actionGroups', () => {
const alert = getLicenseExpiration(null as any, null as any, jest.fn(), false);
const alert = getLicenseExpiration(
getUiSettingsService,
monitoringCluster,
getLogger,
ccrEnabled
);
expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION);
expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]);
});
it('should return the state if no license is provided', async () => {
const result = await setupAlert(null, 0, null);
expect(result[clusterUuid].ui).toEqual(defaultUiState);
const alert = getLicenseExpiration(
getUiSettingsService,
monitoringCluster,
getLogger,
ccrEnabled
);
const services: MockServices | AlertServices = {
callCluster: jest.fn(),
alertInstanceFactory: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
};
const state = { foo: 1 };
const result = await alert.executor({
...alertExecutorOptions,
services,
params,
state,
});
expect(result).toEqual(state);
});
it('should log a warning if no email is provided', async () => {
const customGetUiSettingsService: any = () => ({
asScopedToClient: () => ({
get: () => null,
}),
});
const alert = getLicenseExpiration(
customGetUiSettingsService,
monitoringCluster,
getLogger,
ccrEnabled
);
const services = {
callCluster: jest.fn(
(method: string, { filterPath }): Promise<any> => {
return new Promise(resolve => {
if (filterPath.includes('hits.hits._source.license.*')) {
resolve(
fillLicense({
status: 'good',
type: 'basic',
expiry_date_in_millis: moment()
.add(7, 'days')
.valueOf(),
})
);
}
resolve({});
});
}
),
alertInstanceFactory: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
};
const state = {};
await alert.executor({
...alertExecutorOptions,
services,
params,
state,
});
expect((logger.warn as jest.Mock).mock.calls.length).toBe(1);
expect(logger.warn).toHaveBeenCalledWith(
`Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.`
);
});
it('should fire actions if going to expire', async () => {
const expiryDateMS = moment()
.add(7, 'days')
.valueOf();
const license = {
status: 'active',
type: 'gold',
expiryDateMS,
clusterUuid,
};
const result = await setupAlert(license, 0);
const newState = result[clusterUuid] as AlertLicensePerClusterState;
expect(newState.expiredCheckDateMS > 0).toBe(true);
expect(executeActions).toHaveBeenCalledWith(
undefined,
cluster,
moment.utc(expiryDateMS),
dateFormat,
emailAddress
const scheduleActions = jest.fn();
const alertInstanceFactory = jest.fn(
(id: string): AlertInstance => {
const instance = new AlertInstance();
instance.scheduleActions = scheduleActions;
return instance;
}
);
const alert = getLicenseExpiration(
getUiSettingsService,
monitoringCluster,
getLogger,
ccrEnabled
);
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockReturnValue(
new Promise(resolve => {
const savedObject: SavedObject<SavedObjectAttributes> = {
id: '',
type: '',
references: [],
attributes: {
[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress,
},
};
resolve(savedObject);
})
);
const services = {
callCluster: jest.fn(
(method: string, { filterPath }): Promise<any> => {
return new Promise(resolve => {
if (filterPath.includes('hits.hits._source.license.*')) {
resolve(
fillLicense(
{
status: 'active',
type: 'gold',
expiry_date_in_millis: moment()
.add(7, 'days')
.valueOf(),
},
clusterUuid
)
);
}
resolve({});
});
}
),
alertInstanceFactory,
savedObjectsClient,
};
const state = {};
const result: AlertState = (await alert.executor({
...alertExecutorOptions,
services,
params,
state,
})) as AlertState;
const newState: AlertClusterState = result[clusterUuid] as AlertClusterState;
expect(newState.expiredCheckDateMS > 0).toBe(true);
expect(scheduleActions.mock.calls.length).toBe(1);
expect(scheduleActions.mock.calls[0][1].subject).toBe(
'NEW X-Pack Monitoring: License Expiration'
);
expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress);
});
it('should fire actions if the user fixed their license', async () => {
const expiryDateMS = moment()
.add(365, 'days')
.valueOf();
const license = {
status: 'active',
type: 'gold',
expiryDateMS,
clusterUuid,
};
const result = await setupAlert(license, 100);
const newState = result[clusterUuid] as AlertLicensePerClusterState;
expect(newState.expiredCheckDateMS).toBe(0);
expect(executeActions).toHaveBeenCalledWith(
undefined,
cluster,
moment.utc(expiryDateMS),
dateFormat,
emailAddress,
true
const scheduleActions = jest.fn();
const alertInstanceFactory = jest.fn(
(id: string): AlertInstance => {
const instance = new AlertInstance();
instance.scheduleActions = scheduleActions;
return instance;
}
);
const alert = getLicenseExpiration(
getUiSettingsService,
monitoringCluster,
getLogger,
ccrEnabled
);
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockReturnValue(
new Promise(resolve => {
const savedObject: SavedObject<SavedObjectAttributes> = {
id: '',
type: '',
references: [],
attributes: {
[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress,
},
};
resolve(savedObject);
})
);
const services = {
callCluster: jest.fn(
(method: string, { filterPath }): Promise<any> => {
return new Promise(resolve => {
if (filterPath.includes('hits.hits._source.license.*')) {
resolve(
fillLicense(
{
status: 'active',
type: 'gold',
expiry_date_in_millis: moment()
.add(120, 'days')
.valueOf(),
},
clusterUuid
)
);
}
resolve({});
});
}
),
alertInstanceFactory,
savedObjectsClient,
};
const state: AlertState = {
[clusterUuid]: {
expiredCheckDateMS: moment()
.subtract(1, 'day')
.valueOf(),
ui: { isFiring: true, severity: 0, message: null, resolvedMS: 0, expirationTime: 0 },
},
};
const result: AlertState = (await alert.executor({
...alertExecutorOptions,
services,
params,
state,
})) as AlertState;
const newState: AlertClusterState = result[clusterUuid] as AlertClusterState;
expect(newState.expiredCheckDateMS).toBe(0);
expect(scheduleActions.mock.calls.length).toBe(1);
expect(scheduleActions.mock.calls[0][1].subject).toBe(
'RESOLVED X-Pack Monitoring: License Expiration'
);
expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress);
});
it('should not fire actions for trial license that expire in more than 14 days', async () => {
const expiryDateMS = moment()
.add(20, 'days')
.valueOf();
const license = {
status: 'active',
type: 'trial',
expiryDateMS,
clusterUuid,
const scheduleActions = jest.fn();
const alertInstanceFactory = jest.fn(
(id: string): AlertInstance => {
const instance = new AlertInstance();
instance.scheduleActions = scheduleActions;
return instance;
}
);
const alert = getLicenseExpiration(
getUiSettingsService,
monitoringCluster,
getLogger,
ccrEnabled
);
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockReturnValue(
new Promise(resolve => {
const savedObject: SavedObject<SavedObjectAttributes> = {
id: '',
type: '',
references: [],
attributes: {
[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress,
},
};
resolve(savedObject);
})
);
const services = {
callCluster: jest.fn(
(method: string, { filterPath }): Promise<any> => {
return new Promise(resolve => {
if (filterPath.includes('hits.hits._source.license.*')) {
resolve(
fillLicense(
{
status: 'active',
type: 'trial',
expiry_date_in_millis: moment()
.add(15, 'days')
.valueOf(),
},
clusterUuid
)
);
}
resolve({});
});
}
),
alertInstanceFactory,
savedObjectsClient,
};
const result = await setupAlert(license, 0);
const newState = result[clusterUuid] as AlertLicensePerClusterState;
expect(newState.expiredCheckDateMS).toBe(0);
expect(executeActions).not.toHaveBeenCalled();
const state = {};
const result: AlertState = (await alert.executor({
...alertExecutorOptions,
services,
params,
state,
})) as AlertState;
const newState: AlertClusterState = result[clusterUuid] as AlertClusterState;
expect(newState.expiredCheckDateMS).toBe(undefined);
expect(scheduleActions).not.toHaveBeenCalled();
});
it('should fire actions for trial license that in 14 days or less', async () => {
const expiryDateMS = moment()
.add(7, 'days')
.valueOf();
const license = {
status: 'active',
type: 'trial',
expiryDateMS,
clusterUuid,
};
const result = await setupAlert(license, 0);
const newState = result[clusterUuid] as AlertLicensePerClusterState;
expect(newState.expiredCheckDateMS > 0).toBe(true);
expect(executeActions).toHaveBeenCalledWith(
undefined,
cluster,
moment.utc(expiryDateMS),
dateFormat,
emailAddress
const scheduleActions = jest.fn();
const alertInstanceFactory = jest.fn(
(id: string): AlertInstance => {
const instance = new AlertInstance();
instance.scheduleActions = scheduleActions;
return instance;
}
);
const alert = getLicenseExpiration(
getUiSettingsService,
monitoringCluster,
getLogger,
ccrEnabled
);
const savedObjectsClient = savedObjectsClientMock.create();
savedObjectsClient.get.mockReturnValue(
new Promise(resolve => {
const savedObject: SavedObject<SavedObjectAttributes> = {
id: '',
type: '',
references: [],
attributes: {
[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress,
},
};
resolve(savedObject);
})
);
const services = {
callCluster: jest.fn(
(method: string, { filterPath }): Promise<any> => {
return new Promise(resolve => {
if (filterPath.includes('hits.hits._source.license.*')) {
resolve(
fillLicense(
{
status: 'active',
type: 'trial',
expiry_date_in_millis: moment()
.add(13, 'days')
.valueOf(),
},
clusterUuid
)
);
}
resolve({});
});
}
),
alertInstanceFactory,
savedObjectsClient,
};
const state = {};
const result: AlertState = (await alert.executor({
...alertExecutorOptions,
services,
params,
state,
})) as AlertState;
const newState: AlertClusterState = result[clusterUuid] as AlertClusterState;
expect(newState.expiredCheckDateMS > 0).toBe(true);
expect(scheduleActions.mock.calls.length).toBe(1);
});
});

View file

@ -5,20 +5,24 @@
*/
import moment from 'moment-timezone';
import { get } from 'lodash';
import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server';
import { i18n } from '@kbn/i18n';
import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants';
import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants';
import { AlertType } from '../../../../plugins/alerting/server';
import { fetchLicenses } from '../lib/alerts/fetch_licenses';
import { fetchDefaultEmailAddress } from '../lib/alerts/fetch_default_email_address';
import { fetchClusters } from '../lib/alerts/fetch_clusters';
import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs';
import {
AlertCommonState,
AlertLicensePerClusterState,
AlertCommonExecutorOptions,
AlertCommonCluster,
AlertLicensePerClusterUiState,
AlertLicense,
AlertState,
AlertClusterState,
AlertClusterUiState,
LicenseExpirationAlertExecutorOptions,
} from './types';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib';
import { getPreparedAlert } from '../lib/alerts/get_prepared_alert';
const EXPIRES_DAYS = [60, 30, 14, 7];
@ -28,6 +32,14 @@ export const getLicenseExpiration = (
getLogger: (...scopes: string[]) => Logger,
ccsEnabled: boolean
): AlertType => {
async function getCallCluster(services: any): Promise<any> {
if (!monitoringCluster) {
return services.callCluster;
}
return monitoringCluster.callAsInternalUser;
}
const logger = getLogger(ALERT_TYPE_LICENSE_EXPIRATION);
return {
id: ALERT_TYPE_LICENSE_EXPIRATION,
@ -41,50 +53,54 @@ export const getLicenseExpiration = (
},
],
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertCommonExecutorOptions): Promise<any> {
async executor({
services,
params,
state,
}: LicenseExpirationAlertExecutorOptions): Promise<any> {
logger.debug(
`Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}`
);
const preparedAlert = await getPreparedAlert(
ALERT_TYPE_LICENSE_EXPIRATION,
getUiSettingsService,
monitoringCluster,
logger,
ccsEnabled,
services,
fetchLicenses
);
const callCluster = await getCallCluster(services);
if (!preparedAlert) {
// Support CCS use cases by querying to find available remote clusters
// and then adding those to the index pattern we are searching against
let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH;
if (ccsEnabled) {
const availableCcs = await fetchAvailableCcs(callCluster);
if (availableCcs.length > 0) {
esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs);
}
}
const clusters = await fetchClusters(callCluster, esIndexPattern);
// Fetch licensing information from cluster_stats documents
const licenses: AlertLicense[] = await fetchLicenses(callCluster, clusters, esIndexPattern);
if (licenses.length === 0) {
logger.warn(`No license found for ${ALERT_TYPE_LICENSE_EXPIRATION}.`);
return state;
}
const { emailAddress, data: licenses, clusters, dateFormat } = preparedAlert;
const uiSettings = (await getUiSettingsService()).asScopedToClient(
services.savedObjectsClient
);
const dateFormat: string = await uiSettings.get<string>('dateFormat');
const timezone: string = await uiSettings.get<string>('dateFormat:tz');
const emailAddress = await fetchDefaultEmailAddress(uiSettings);
if (!emailAddress) {
// TODO: we can do more here
logger.warn(
`Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.`
);
return;
}
const result: AlertCommonState = { ...state };
const defaultAlertState: AlertLicensePerClusterState = {
expiredCheckDateMS: 0,
ui: {
isFiring: false,
message: null,
severity: 0,
resolvedMS: 0,
lastCheckedMS: 0,
triggeredMS: 0,
},
};
const result: AlertState = { ...state };
for (const license of licenses) {
const alertState: AlertLicensePerClusterState =
(state[license.clusterUuid] as AlertLicensePerClusterState) || defaultAlertState;
const cluster = clusters.find(
(c: AlertCommonCluster) => c.clusterUuid === license.clusterUuid
);
if (!cluster) {
logger.warn(`Unable to find cluster for clusterUuid='${license.clusterUuid}'`);
continue;
}
const licenseState: AlertClusterState = state[license.clusterUuid] || {};
const $expiry = moment.utc(license.expiryDateMS);
let isExpired = false;
let severity = 0;
@ -107,26 +123,31 @@ export const getLicenseExpiration = (
}
}
const ui = alertState.ui;
let triggered = ui.triggeredMS;
const ui: AlertClusterUiState = get<AlertClusterUiState>(licenseState, 'ui', {
isFiring: false,
message: null,
severity: 0,
resolvedMS: 0,
expirationTime: 0,
});
let resolved = ui.resolvedMS;
let message = ui.message;
let expiredCheckDate = alertState.expiredCheckDateMS;
let expiredCheckDate = licenseState.expiredCheckDateMS;
const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION);
if (isExpired) {
if (!alertState.expiredCheckDateMS) {
if (!licenseState.expiredCheckDateMS) {
logger.debug(`License will expire soon, sending email`);
executeActions(instance, cluster, $expiry, dateFormat, emailAddress);
expiredCheckDate = triggered = moment().valueOf();
executeActions(instance, license, $expiry, dateFormat, emailAddress);
expiredCheckDate = moment().valueOf();
}
message = getUiMessage();
message = getUiMessage(license, timezone);
resolved = 0;
} else if (!isExpired && alertState.expiredCheckDateMS) {
} else if (!isExpired && licenseState.expiredCheckDateMS) {
logger.debug(`License expiration has been resolved, sending email`);
executeActions(instance, cluster, $expiry, dateFormat, emailAddress, true);
executeActions(instance, license, $expiry, dateFormat, emailAddress, true);
expiredCheckDate = 0;
message = getUiMessage(true);
message = getUiMessage(license, timezone, true);
resolved = moment().valueOf();
}
@ -138,10 +159,8 @@ export const getLicenseExpiration = (
isFiring: expiredCheckDate > 0,
severity,
resolvedMS: resolved,
triggeredMS: triggered,
lastCheckedMS: moment().valueOf(),
} as AlertLicensePerClusterUiState,
} as AlertLicensePerClusterState;
},
};
}
return result;

View file

@ -5,79 +5,41 @@
*/
import { Moment } from 'moment';
import { AlertExecutorOptions } from '../../../alerting/server';
import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from './enums';
export interface AlertLicense {
status: string;
type: string;
expiryDateMS: number;
clusterUuid: string;
}
export interface AlertClusterState {
state: AlertClusterStateState;
clusterUuid: string;
}
export interface AlertCommonState {
[clusterUuid: string]: AlertCommonPerClusterState;
}
export interface AlertCommonPerClusterState {
ui: AlertCommonPerClusterUiState;
}
export interface AlertClusterStatePerClusterState extends AlertCommonPerClusterState {
state: AlertClusterStateState;
}
export interface AlertLicensePerClusterState extends AlertCommonPerClusterState {
expiredCheckDateMS: number;
}
export interface AlertCommonPerClusterUiState {
isFiring: boolean;
severity: number;
message: AlertCommonPerClusterMessage | null;
resolvedMS: number;
lastCheckedMS: number;
triggeredMS: number;
}
export interface AlertCommonPerClusterMessage {
text: string; // Do this. #link this is a link #link
tokens?: AlertCommonPerClusterMessageToken[];
}
export interface AlertCommonPerClusterMessageToken {
startToken: string;
endToken?: string;
type: AlertCommonPerClusterMessageTokenType;
}
export interface AlertCommonPerClusterMessageLinkToken extends AlertCommonPerClusterMessageToken {
url?: string;
}
export interface AlertCommonPerClusterMessageTimeToken extends AlertCommonPerClusterMessageToken {
isRelative: boolean;
isAbsolute: boolean;
}
export interface AlertLicensePerClusterUiState extends AlertCommonPerClusterUiState {
expirationTime: number;
}
export interface AlertCommonCluster {
clusterUuid: string;
clusterName: string;
}
export interface AlertCommonExecutorOptions extends AlertExecutorOptions {
state: AlertCommonState;
export interface AlertState {
[clusterUuid: string]: AlertClusterState;
}
export interface AlertCommonParams {
export interface AlertClusterState {
expiredCheckDateMS: number | Moment;
ui: AlertClusterUiState;
}
export interface AlertClusterUiState {
isFiring: boolean;
severity: number;
message: string | null;
resolvedMS: number;
expirationTime: number;
}
export interface AlertCluster {
clusterUuid: string;
}
export interface LicenseExpirationAlertExecutorOptions extends AlertExecutorOptions {
state: AlertState;
}
export interface AlertParams {
dateFormat: string;
timezone: string;
}

View file

@ -1,70 +0,0 @@
/*
* 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 { executeActions, getUiMessage } from './cluster_state.lib';
import { AlertClusterStateState } from '../../alerts/enums';
import { AlertCommonPerClusterMessageLinkToken } from '../../alerts/types';
describe('clusterState lib', () => {
describe('executeActions', () => {
const clusterName = 'clusterA';
const instance: any = { scheduleActions: jest.fn() };
const license: any = { clusterName };
const status = AlertClusterStateState.Green;
const emailAddress = 'test@test.com';
beforeEach(() => {
instance.scheduleActions.mockClear();
});
it('should schedule actions when firing', () => {
executeActions(instance, license, status, emailAddress, false);
expect(instance.scheduleActions).toHaveBeenCalledWith('default', {
subject: 'NEW X-Pack Monitoring: Cluster Status',
message: `Allocate missing replica shards for cluster '${clusterName}'`,
to: emailAddress,
});
});
it('should have a different message for red state', () => {
executeActions(instance, license, AlertClusterStateState.Red, emailAddress, false);
expect(instance.scheduleActions).toHaveBeenCalledWith('default', {
subject: 'NEW X-Pack Monitoring: Cluster Status',
message: `Allocate missing primary and replica shards for cluster '${clusterName}'`,
to: emailAddress,
});
});
it('should schedule actions when resolved', () => {
executeActions(instance, license, status, emailAddress, true);
expect(instance.scheduleActions).toHaveBeenCalledWith('default', {
subject: 'RESOLVED X-Pack Monitoring: Cluster Status',
message: `This cluster alert has been resolved: Allocate missing replica shards for cluster '${clusterName}'`,
to: emailAddress,
});
});
});
describe('getUiMessage', () => {
it('should return a message when firing', () => {
const message = getUiMessage(AlertClusterStateState.Red, false);
expect(message.text).toBe(
`Elasticsearch cluster status is red. #start_linkAllocate missing primary and replica shards#end_link`
);
expect(message.tokens && message.tokens.length).toBe(1);
expect(message.tokens && message.tokens[0].startToken).toBe('#start_link');
expect(message.tokens && message.tokens[0].endToken).toBe('#end_link');
expect(
message.tokens && (message.tokens[0] as AlertCommonPerClusterMessageLinkToken).url
).toBe('elasticsearch/indices');
});
it('should return a message when resolved', () => {
const message = getUiMessage(AlertClusterStateState.Green, true);
expect(message.text).toBe(`Elasticsearch cluster status is green.`);
expect(message.tokens).not.toBeDefined();
});
});
});

View file

@ -1,88 +0,0 @@
/*
* 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';
import { AlertInstance } from '../../../../alerting/server';
import {
AlertCommonCluster,
AlertCommonPerClusterMessage,
AlertCommonPerClusterMessageLinkToken,
} from '../../alerts/types';
import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from '../../alerts/enums';
const RESOLVED_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.resolvedSubject', {
defaultMessage: 'RESOLVED X-Pack Monitoring: Cluster Status',
});
const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.newSubject', {
defaultMessage: 'NEW X-Pack Monitoring: Cluster Status',
});
const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterStatus.redMessage', {
defaultMessage: 'Allocate missing primary and replica shards',
});
const YELLOW_STATUS_MESSAGE = i18n.translate(
'xpack.monitoring.alerts.clusterStatus.yellowMessage',
{
defaultMessage: 'Allocate missing replica shards',
}
);
export function executeActions(
instance: AlertInstance,
cluster: AlertCommonCluster,
status: AlertClusterStateState,
emailAddress: string,
resolved: boolean = false
) {
const message =
status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE;
if (resolved) {
instance.scheduleActions('default', {
subject: RESOLVED_SUBJECT,
message: `This cluster alert has been resolved: ${message} for cluster '${cluster.clusterName}'`,
to: emailAddress,
});
} else {
instance.scheduleActions('default', {
subject: NEW_SUBJECT,
message: `${message} for cluster '${cluster.clusterName}'`,
to: emailAddress,
});
}
}
export function getUiMessage(
status: AlertClusterStateState,
resolved: boolean = false
): AlertCommonPerClusterMessage {
if (resolved) {
return {
text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage', {
defaultMessage: `Elasticsearch cluster status is green.`,
}),
};
}
const message =
status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE;
return {
text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.firingMessage', {
defaultMessage: `Elasticsearch cluster status is {status}. #start_link{message}#end_link`,
values: {
status,
message,
},
}),
tokens: [
{
startToken: '#start_link',
endToken: '#end_link',
type: AlertCommonPerClusterMessageTokenType.Link,
url: 'elasticsearch/indices',
} as AlertCommonPerClusterMessageLinkToken,
],
};
}

View file

@ -1,39 +0,0 @@
/*
* 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 { fetchClusterState } from './fetch_cluster_state';
describe('fetchClusterState', () => {
it('should return the cluster state', async () => {
const status = 'green';
const clusterUuid = 'sdfdsaj34434';
const callCluster = jest.fn(() => ({
hits: {
hits: [
{
_source: {
cluster_state: {
status,
},
cluster_uuid: clusterUuid,
},
},
],
},
}));
const clusters = [{ clusterUuid, clusterName: 'foo' }];
const index = '.monitoring-es-*';
const state = await fetchClusterState(callCluster, clusters, index);
expect(state).toEqual([
{
state: status,
clusterUuid,
},
]);
});
});

View file

@ -1,53 +0,0 @@
/*
* 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 { get } from 'lodash';
import { AlertCommonCluster, AlertClusterState } from '../../alerts/types';
export async function fetchClusterState(
callCluster: any,
clusters: AlertCommonCluster[],
index: string
): Promise<AlertClusterState[]> {
const params = {
index,
filterPath: ['hits.hits._source.cluster_state.status', 'hits.hits._source.cluster_uuid'],
body: {
size: 1,
sort: [{ timestamp: { order: 'desc' } }],
query: {
bool: {
filter: [
{
terms: {
cluster_uuid: clusters.map(cluster => cluster.clusterUuid),
},
},
{
term: {
type: 'cluster_stats',
},
},
{
range: {
timestamp: {
gte: 'now-2m',
},
},
},
],
},
},
},
};
const response = await callCluster('search', params);
return get<any>(response, 'hits.hits', []).map((hit: any) => {
return {
state: get(hit, '_source.cluster_state.status'),
clusterUuid: get(hit, '_source.cluster_uuid'),
};
});
}

View file

@ -6,51 +6,21 @@
import { fetchClusters } from './fetch_clusters';
describe('fetchClusters', () => {
const clusterUuid = '1sdfds734';
const clusterName = 'monitoring';
it('return a list of clusters', async () => {
const callCluster = jest.fn().mockImplementation(() => ({
hits: {
hits: [
{
_source: {
cluster_uuid: clusterUuid,
cluster_name: clusterName,
aggregations: {
clusters: {
buckets: [
{
key: 'clusterA',
},
},
],
],
},
},
}));
const index = '.monitoring-es-*';
const result = await fetchClusters(callCluster, index);
expect(result).toEqual([{ clusterUuid, clusterName }]);
});
it('return the metadata name if available', async () => {
const metadataName = 'custom-monitoring';
const callCluster = jest.fn().mockImplementation(() => ({
hits: {
hits: [
{
_source: {
cluster_uuid: clusterUuid,
cluster_name: clusterName,
cluster_settings: {
cluster: {
metadata: {
display_name: metadataName,
},
},
},
},
},
],
},
}));
const index = '.monitoring-es-*';
const result = await fetchClusters(callCluster, index);
expect(result).toEqual([{ clusterUuid, clusterName: metadataName }]);
expect(result).toEqual([{ clusterUuid: 'clusterA' }]);
});
it('should limit the time period in the query', async () => {

View file

@ -4,21 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import { AlertCommonCluster } from '../../alerts/types';
import { AlertCluster } from '../../alerts/types';
export async function fetchClusters(
callCluster: any,
index: string
): Promise<AlertCommonCluster[]> {
interface AggregationResult {
key: string;
}
export async function fetchClusters(callCluster: any, index: string): Promise<AlertCluster[]> {
const params = {
index,
filterPath: [
'hits.hits._source.cluster_settings.cluster.metadata.display_name',
'hits.hits._source.cluster_uuid',
'hits.hits._source.cluster_name',
],
filterPath: 'aggregations.clusters.buckets',
body: {
size: 1000,
size: 0,
query: {
bool: {
filter: [
@ -37,21 +34,19 @@ export async function fetchClusters(
],
},
},
collapse: {
field: 'cluster_uuid',
aggs: {
clusters: {
terms: {
field: 'cluster_uuid',
size: 1000,
},
},
},
},
};
const response = await callCluster('search', params);
return get(response, 'hits.hits', []).map((hit: any) => {
const clusterName: string =
get(hit, '_source.cluster_settings.cluster.metadata.display_name') ||
get(hit, '_source.cluster_name') ||
get(hit, '_source.cluster_uuid');
return {
clusterUuid: get(hit, '_source.cluster_uuid'),
clusterName,
};
});
return get(response, 'aggregations.clusters.buckets', []).map((bucket: AggregationResult) => ({
clusterUuid: bucket.key,
}));
}

View file

@ -6,28 +6,28 @@
import { fetchLicenses } from './fetch_licenses';
describe('fetchLicenses', () => {
const clusterName = 'MyCluster';
const clusterUuid = 'clusterA';
const license = {
status: 'active',
expiry_date_in_millis: 1579532493876,
type: 'basic',
};
it('return a list of licenses', async () => {
const clusterName = 'MyCluster';
const clusterUuid = 'clusterA';
const license = {
status: 'active',
expiry_date_in_millis: 1579532493876,
type: 'basic',
};
const callCluster = jest.fn().mockImplementation(() => ({
hits: {
hits: [
{
_source: {
license,
cluster_name: clusterName,
cluster_uuid: clusterUuid,
},
},
],
},
}));
const clusters = [{ clusterUuid, clusterName }];
const clusters = [{ clusterUuid }];
const index = '.monitoring-es-*';
const result = await fetchLicenses(callCluster, clusters, index);
expect(result).toEqual([
@ -36,13 +36,15 @@ describe('fetchLicenses', () => {
type: license.type,
expiryDateMS: license.expiry_date_in_millis,
clusterUuid,
clusterName,
},
]);
});
it('should only search for the clusters provided', async () => {
const clusterUuid = 'clusterA';
const callCluster = jest.fn();
const clusters = [{ clusterUuid, clusterName }];
const clusters = [{ clusterUuid }];
const index = '.monitoring-es-*';
await fetchLicenses(callCluster, clusters, index);
const params = callCluster.mock.calls[0][1];
@ -50,11 +52,54 @@ describe('fetchLicenses', () => {
});
it('should limit the time period in the query', async () => {
const clusterUuid = 'clusterA';
const callCluster = jest.fn();
const clusters = [{ clusterUuid, clusterName }];
const clusters = [{ clusterUuid }];
const index = '.monitoring-es-*';
await fetchLicenses(callCluster, clusters, index);
const params = callCluster.mock.calls[0][1];
expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m');
});
it('should give priority to the metadata name', async () => {
const clusterName = 'MyCluster';
const clusterUuid = 'clusterA';
const license = {
status: 'active',
expiry_date_in_millis: 1579532493876,
type: 'basic',
};
const callCluster = jest.fn().mockImplementation(() => ({
hits: {
hits: [
{
_source: {
license,
cluster_name: 'fakeName',
cluster_uuid: clusterUuid,
cluster_settings: {
cluster: {
metadata: {
display_name: clusterName,
},
},
},
},
},
],
},
}));
const clusters = [{ clusterUuid }];
const index = '.monitoring-es-*';
const result = await fetchLicenses(callCluster, clusters, index);
expect(result).toEqual([
{
status: license.status,
type: license.type,
expiryDateMS: license.expiry_date_in_millis,
clusterUuid,
clusterName,
},
]);
});
});

View file

@ -4,16 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import { AlertLicense, AlertCommonCluster } from '../../alerts/types';
import { AlertLicense, AlertCluster } from '../../alerts/types';
export async function fetchLicenses(
callCluster: any,
clusters: AlertCommonCluster[],
clusters: AlertCluster[],
index: string
): Promise<AlertLicense[]> {
const params = {
index,
filterPath: ['hits.hits._source.license.*', 'hits.hits._source.cluster_uuid'],
filterPath: [
'hits.hits._source.license.*',
'hits.hits._source.cluster_settings.cluster.metadata.display_name',
'hits.hits._source.cluster_uuid',
'hits.hits._source.cluster_name',
],
body: {
size: 1,
sort: [{ timestamp: { order: 'desc' } }],
@ -45,12 +50,17 @@ export async function fetchLicenses(
const response = await callCluster('search', params);
return get<any>(response, 'hits.hits', []).map((hit: any) => {
const clusterName: string =
get(hit, '_source.cluster_settings.cluster.metadata.display_name') ||
get(hit, '_source.cluster_name') ||
get(hit, '_source.cluster_uuid');
const rawLicense: any = get(hit, '_source.license', {});
const license: AlertLicense = {
status: rawLicense.status,
type: rawLicense.type,
expiryDateMS: rawLicense.expiry_date_in_millis,
clusterUuid: get(hit, '_source.cluster_uuid'),
clusterName,
};
return license;
});

View file

@ -1,122 +0,0 @@
/*
* 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 { fetchStatus } from './fetch_status';
import { AlertCommonPerClusterState } from '../../alerts/types';
describe('fetchStatus', () => {
const alertType = 'monitoringTest';
const log = { warn: jest.fn() };
const start = 0;
const end = 0;
const id = 1;
const defaultUiState = {
isFiring: false,
severity: 0,
message: null,
resolvedMS: 0,
lastCheckedMS: 0,
triggeredMS: 0,
};
const alertsClient = {
find: jest.fn(() => ({
total: 1,
data: [
{
id,
},
],
})),
getAlertState: jest.fn(() => ({
alertTypeState: {
state: {
ui: defaultUiState,
} as AlertCommonPerClusterState,
},
})),
};
afterEach(() => {
(alertsClient.find as jest.Mock).mockClear();
(alertsClient.getAlertState as jest.Mock).mockClear();
});
it('should fetch from the alerts client', async () => {
const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any);
expect(status).toEqual([]);
});
it('should return alerts that are firing', async () => {
alertsClient.getAlertState = jest.fn(() => ({
alertTypeState: {
state: {
ui: {
...defaultUiState,
isFiring: true,
},
} as AlertCommonPerClusterState,
},
}));
const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any);
expect(status.length).toBe(1);
expect(status[0].type).toBe(alertType);
expect(status[0].isFiring).toBe(true);
});
it('should return alerts that have been resolved in the time period', async () => {
alertsClient.getAlertState = jest.fn(() => ({
alertTypeState: {
state: {
ui: {
...defaultUiState,
resolvedMS: 1500,
},
} as AlertCommonPerClusterState,
},
}));
const customStart = 1000;
const customEnd = 2000;
const status = await fetchStatus(
alertsClient as any,
[alertType],
customStart,
customEnd,
log as any
);
expect(status.length).toBe(1);
expect(status[0].type).toBe(alertType);
expect(status[0].isFiring).toBe(false);
});
it('should pass in the right filter to the alerts client', async () => {
await fetchStatus(alertsClient as any, [alertType], start, end, log as any);
expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe(
`alert.attributes.alertTypeId:${alertType}`
);
});
it('should return nothing if no alert state is found', async () => {
alertsClient.getAlertState = jest.fn(() => ({
alertTypeState: null,
})) as any;
const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any);
expect(status).toEqual([]);
});
it('should return nothing if no alerts are found', async () => {
alertsClient.find = jest.fn(() => ({
total: 0,
data: [],
})) as any;
const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any);
expect(status).toEqual([]);
});
});

View file

@ -4,53 +4,81 @@
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
import { Logger } from '../../../../../../src/core/server';
import { AlertCommonPerClusterState } from '../../alerts/types';
import { AlertsClient } from '../../../../alerting/server';
import { get } from 'lodash';
import { AlertClusterState } from '../../alerts/types';
import { ALERT_TYPES, LOGGING_TAG } from '../../../common/constants';
export async function fetchStatus(
alertsClient: AlertsClient,
alertTypes: string[],
callCluster: any,
start: number,
end: number,
log: Logger
clusterUuid: string,
server: any
): Promise<any[]> {
// TODO: this shouldn't query task manager directly but rather
// use an api exposed by the alerting/actions plugin
// See https://github.com/elastic/kibana/issues/48442
const statuses = await Promise.all(
alertTypes.map(
ALERT_TYPES.map(
type =>
new Promise(async (resolve, reject) => {
// We need to get the id from the alertTypeId
const alerts = await alertsClient.find({
options: {
filter: `alert.attributes.alertTypeId:${type}`,
},
});
if (alerts.total === 0) {
try {
const params = {
index: '.kibana_task_manager',
filterPath: ['hits.hits._source.task.state'],
body: {
size: 1,
sort: [{ updated_at: { order: 'desc' } }],
query: {
bool: {
filter: [
{
term: {
'task.taskType': `alerting:${type}`,
},
},
],
},
},
},
};
const response = await callCluster('search', params);
const state = get(response, 'hits.hits[0]._source.task.state', '{}');
const clusterState: AlertClusterState = get<AlertClusterState>(
JSON.parse(state),
`alertTypeState.${clusterUuid}`,
{
expiredCheckDateMS: 0,
ui: {
isFiring: false,
message: null,
severity: 0,
resolvedMS: 0,
expirationTime: 0,
},
}
);
const isInBetween = moment(clusterState.ui.resolvedMS).isBetween(start, end);
if (clusterState.ui.isFiring || isInBetween) {
return resolve({
type,
...clusterState.ui,
});
}
return resolve(false);
} catch (err) {
const reason = get(err, 'body.error.type');
if (reason === 'index_not_found_exception') {
server.log(
['error', LOGGING_TAG],
`Unable to fetch alerts. Alerts depends on task manager, which has not been started yet.`
);
} else {
server.log(['error', LOGGING_TAG], err.message);
}
return resolve(false);
}
if (alerts.total !== 1) {
log.warn(`Found more than one alert for type ${type} which is unexpected.`);
}
const id = alerts.data[0].id;
// Now that we have the id, we can get the state
const states = await alertsClient.getAlertState({ id });
if (!states || !states.alertTypeState) {
log.warn(`No alert states found for type ${type} which is unexpected.`);
return resolve(false);
}
const state = Object.values(states.alertTypeState)[0] as AlertCommonPerClusterState;
const isInBetween = moment(state.ui.resolvedMS).isBetween(start, end);
if (state.ui.isFiring || isInBetween) {
return resolve({
type,
...state.ui,
});
}
return resolve(false);
})
)
);

View file

@ -1,163 +0,0 @@
/*
* 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 { getPreparedAlert } from './get_prepared_alert';
import { fetchClusters } from './fetch_clusters';
import { fetchDefaultEmailAddress } from './fetch_default_email_address';
jest.mock('./fetch_clusters', () => ({
fetchClusters: jest.fn(),
}));
jest.mock('./fetch_default_email_address', () => ({
fetchDefaultEmailAddress: jest.fn(),
}));
describe('getPreparedAlert', () => {
const uiSettings = { get: jest.fn() };
const alertType = 'test';
const getUiSettingsService = async () => ({
asScopedToClient: () => uiSettings,
});
const monitoringCluster = null;
const logger = { warn: jest.fn() };
const ccsEnabled = false;
const services = {
callCluster: jest.fn(),
savedObjectsClient: null,
};
const emailAddress = 'foo@foo.com';
const data = [{ foo: 1 }];
const dataFetcher = () => data;
const clusterName = 'MonitoringCluster';
const clusterUuid = 'sdf34sdf';
const clusters = [{ clusterName, clusterUuid }];
afterEach(() => {
(uiSettings.get as jest.Mock).mockClear();
(services.callCluster as jest.Mock).mockClear();
(fetchClusters as jest.Mock).mockClear();
(fetchDefaultEmailAddress as jest.Mock).mockClear();
});
beforeEach(() => {
(fetchClusters as jest.Mock).mockImplementation(() => clusters);
(fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => emailAddress);
});
it('should return fields as expected', async () => {
(uiSettings.get as jest.Mock).mockImplementation(() => {
return emailAddress;
});
const alert = await getPreparedAlert(
alertType,
getUiSettingsService as any,
monitoringCluster as any,
logger as any,
ccsEnabled,
services as any,
dataFetcher as any
);
expect(alert && alert.emailAddress).toBe(emailAddress);
expect(alert && alert.data).toBe(data);
});
it('should add ccs if specified', async () => {
const ccsClusterName = 'remoteCluster';
(services.callCluster as jest.Mock).mockImplementation(() => {
return {
[ccsClusterName]: {
connected: true,
},
};
});
await getPreparedAlert(
alertType,
getUiSettingsService as any,
monitoringCluster as any,
logger as any,
true,
services as any,
dataFetcher as any
);
expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(true);
});
it('should ignore ccs if no remote clusters are available', async () => {
const ccsClusterName = 'remoteCluster';
(services.callCluster as jest.Mock).mockImplementation(() => {
return {
[ccsClusterName]: {
connected: false,
},
};
});
await getPreparedAlert(
alertType,
getUiSettingsService as any,
monitoringCluster as any,
logger as any,
true,
services as any,
dataFetcher as any
);
expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(false);
});
it('should pass in the clusters into the data fetcher', async () => {
const customDataFetcher = jest.fn(() => data);
await getPreparedAlert(
alertType,
getUiSettingsService as any,
monitoringCluster as any,
logger as any,
true,
services as any,
customDataFetcher as any
);
expect((customDataFetcher as jest.Mock).mock.calls[0][1]).toBe(clusters);
});
it('should return nothing if the data fetcher returns nothing', async () => {
const customDataFetcher = jest.fn(() => []);
const result = await getPreparedAlert(
alertType,
getUiSettingsService as any,
monitoringCluster as any,
logger as any,
true,
services as any,
customDataFetcher as any
);
expect(result).toBe(null);
});
it('should return nothing if there is no email address', async () => {
(fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => null);
const result = await getPreparedAlert(
alertType,
getUiSettingsService as any,
monitoringCluster as any,
logger as any,
true,
services as any,
dataFetcher as any
);
expect(result).toBe(null);
});
});

View file

@ -1,87 +0,0 @@
/*
* 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 { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'kibana/server';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { AlertServices } from '../../../../alerting/server';
import { AlertCommonCluster } from '../../alerts/types';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants';
import { fetchAvailableCcs } from './fetch_available_ccs';
import { getCcsIndexPattern } from './get_ccs_index_pattern';
import { fetchClusters } from './fetch_clusters';
import { fetchDefaultEmailAddress } from './fetch_default_email_address';
export interface PreparedAlert {
emailAddress: string;
clusters: AlertCommonCluster[];
data: any[];
timezone: string;
dateFormat: string;
}
async function getCallCluster(
monitoringCluster: ICustomClusterClient,
services: Pick<AlertServices, 'callCluster'>
): Promise<any> {
if (!monitoringCluster) {
return services.callCluster;
}
return monitoringCluster.callAsInternalUser;
}
export async function getPreparedAlert(
alertType: string,
getUiSettingsService: () => Promise<UiSettingsServiceStart>,
monitoringCluster: ICustomClusterClient,
logger: Logger,
ccsEnabled: boolean,
services: Pick<AlertServices, 'callCluster' | 'savedObjectsClient'>,
dataFetcher: (
callCluster: CallCluster,
clusters: AlertCommonCluster[],
esIndexPattern: string
) => Promise<any>
): Promise<PreparedAlert | null> {
const callCluster = await getCallCluster(monitoringCluster, services);
// Support CCS use cases by querying to find available remote clusters
// and then adding those to the index pattern we are searching against
let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH;
if (ccsEnabled) {
const availableCcs = await fetchAvailableCcs(callCluster);
if (availableCcs.length > 0) {
esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs);
}
}
const clusters = await fetchClusters(callCluster, esIndexPattern);
// Fetch the specific data
const data = await dataFetcher(callCluster, clusters, esIndexPattern);
if (data.length === 0) {
logger.warn(`No data found for ${alertType}.`);
return null;
}
const uiSettings = (await getUiSettingsService()).asScopedToClient(services.savedObjectsClient);
const dateFormat: string = await uiSettings.get<string>('dateFormat');
const timezone: string = await uiSettings.get<string>('dateFormat:tz');
const emailAddress = await fetchDefaultEmailAddress(uiSettings);
if (!emailAddress) {
// TODO: we can do more here
logger.warn(`Unable to send email for ${alertType} because there is no email configured.`);
return null;
}
return {
emailAddress,
data,
clusters,
dateFormat,
timezone,
};
}

View file

@ -39,26 +39,17 @@ describe('licenseExpiration lib', () => {
});
describe('getUiMessage', () => {
const timezone = 'Europe/London';
const license: any = { expiryDateMS: moment.tz('2020-01-20 08:00:00', timezone).utc() };
it('should return a message when firing', () => {
const message = getUiMessage(false);
expect(message.text).toBe(
`This cluster's license is going to expire in #relative at #absolute. #start_linkPlease update your license#end_link`
);
// LOL How do I avoid this in TS????
if (!message.tokens) {
return expect(false).toBe(true);
}
expect(message.tokens.length).toBe(3);
expect(message.tokens[0].startToken).toBe('#relative');
expect(message.tokens[1].startToken).toBe('#absolute');
expect(message.tokens[2].startToken).toBe('#start_link');
expect(message.tokens[2].endToken).toBe('#end_link');
const message = getUiMessage(license, timezone, false);
expect(message).toBe(`This cluster's license is going to expire in #relative at #absolute.`);
});
it('should return a message when resolved', () => {
const message = getUiMessage(true);
expect(message.text).toBe(`This cluster's license is active.`);
expect(message.tokens).not.toBeDefined();
const message = getUiMessage(license, timezone, true);
expect(message).toBe(`This cluster's license is active.`);
});
});
});

View file

@ -6,13 +6,7 @@
import { Moment } from 'moment-timezone';
import { i18n } from '@kbn/i18n';
import { AlertInstance } from '../../../../alerting/server';
import {
AlertCommonPerClusterMessageLinkToken,
AlertCommonPerClusterMessageTimeToken,
AlertCommonCluster,
AlertCommonPerClusterMessage,
} from '../../alerts/types';
import { AlertCommonPerClusterMessageTokenType } from '../../alerts/enums';
import { AlertLicense } from '../../alerts/types';
const RESOLVED_SUBJECT = i18n.translate(
'xpack.monitoring.alerts.licenseExpiration.resolvedSubject',
@ -27,7 +21,7 @@ const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.ne
export function executeActions(
instance: AlertInstance,
cluster: AlertCommonCluster,
license: AlertLicense,
$expiry: Moment,
dateFormat: string,
emailAddress: string,
@ -37,14 +31,14 @@ export function executeActions(
instance.scheduleActions('default', {
subject: RESOLVED_SUBJECT,
message: `This cluster alert has been resolved: Cluster '${
cluster.clusterName
license.clusterName
}' license was going to expire on ${$expiry.format(dateFormat)}.`,
to: emailAddress,
});
} else {
instance.scheduleActions('default', {
subject: NEW_SUBJECT,
message: `Cluster '${cluster.clusterName}' license is going to expire on ${$expiry.format(
message: `Cluster '${license.clusterName}' license is going to expire on ${$expiry.format(
dateFormat
)}. Please update your license.`,
to: emailAddress,
@ -52,43 +46,13 @@ export function executeActions(
}
}
export function getUiMessage(resolved: boolean = false): AlertCommonPerClusterMessage {
export function getUiMessage(license: AlertLicense, timezone: string, resolved: boolean = false) {
if (resolved) {
return {
text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', {
defaultMessage: `This cluster's license is active.`,
}),
};
return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', {
defaultMessage: `This cluster's license is active.`,
});
}
const linkText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.linkText', {
defaultMessage: 'Please update your license',
return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', {
defaultMessage: `This cluster's license is going to expire in #relative at #absolute.`,
});
return {
text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', {
defaultMessage: `This cluster's license is going to expire in #relative at #absolute. #start_link{linkText}#end_link`,
values: {
linkText,
},
}),
tokens: [
{
startToken: '#relative',
type: AlertCommonPerClusterMessageTokenType.Time,
isRelative: true,
isAbsolute: false,
} as AlertCommonPerClusterMessageTimeToken,
{
startToken: '#absolute',
type: AlertCommonPerClusterMessageTokenType.Time,
isAbsolute: true,
isRelative: false,
} as AlertCommonPerClusterMessageTimeToken,
{
startToken: '#start_link',
endToken: '#end_link',
type: AlertCommonPerClusterMessageTokenType.Link,
url: 'license',
} as AlertCommonPerClusterMessageLinkToken,
],
};
}

View file

@ -29,7 +29,6 @@ import {
CODE_PATH_BEATS,
CODE_PATH_APM,
KIBANA_ALERTING_ENABLED,
ALERT_TYPES,
} from '../../../common/constants';
import { getApmsForClusters } from '../apm/get_apms_for_clusters';
import { i18n } from '@kbn/i18n';
@ -103,8 +102,15 @@ export async function getClustersFromRequest(
if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) {
if (KIBANA_ALERTING_ENABLED) {
const alertsClient = req.getAlertsClient ? req.getAlertsClient() : null;
cluster.alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger);
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const callCluster = (...args) => callWithRequest(req, ...args);
cluster.alerts = await fetchStatus(
callCluster,
start,
end,
cluster.cluster_uuid,
req.server
);
} else {
cluster.alerts = await alertsClusterSearch(
req,

View file

@ -47,7 +47,6 @@ import {
PluginSetupContract as AlertingPluginSetupContract,
} from '../../alerting/server';
import { getLicenseExpiration } from './alerts/license_expiration';
import { getClusterState } from './alerts/cluster_state';
import { InfraPluginSetup } from '../../infra/server';
export interface LegacyAPI {
@ -155,17 +154,6 @@ export class Plugin {
config.ui.ccs.enabled
)
);
plugins.alerting.registerType(
getClusterState(
async () => {
const coreStart = (await core.getStartServices())[0];
return coreStart.uiSettings;
},
cluster,
this.getLogger,
config.ui.ccs.enabled
)
);
}
// Initialize telemetry

View file

@ -8,12 +8,8 @@ import { schema } from '@kbn/config-schema';
import { isFunction } from 'lodash';
import {
ALERT_TYPE_LICENSE_EXPIRATION,
ALERT_TYPE_CLUSTER_STATE,
MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS,
ALERT_TYPES,
} from '../../../../../common/constants';
import { handleError } from '../../../../lib/errors';
import { fetchStatus } from '../../../../lib/alerts/fetch_status';
async function createAlerts(req, alertsClient, { selectedEmailActionId }) {
const createdAlerts = [];
@ -21,21 +17,7 @@ async function createAlerts(req, alertsClient, { selectedEmailActionId }) {
// Create alerts
const ALERT_TYPES = {
[ALERT_TYPE_LICENSE_EXPIRATION]: {
schedule: { interval: '1m' },
actions: [
{
group: 'default',
id: selectedEmailActionId,
params: {
subject: '{{context.subject}}',
message: `{{context.message}}`,
to: ['{{context.to}}'],
},
},
],
},
[ALERT_TYPE_CLUSTER_STATE]: {
schedule: { interval: '1m' },
schedule: { interval: '10s' },
actions: [
{
group: 'default',
@ -104,37 +86,4 @@ export function createKibanaAlertsRoute(server) {
return { alerts, emailResponse };
},
});
server.route({
method: 'POST',
path: '/api/monitoring/v1/alert_status',
config: {
validate: {
payload: schema.object({
timeRange: schema.object({
min: schema.string(),
max: schema.string(),
}),
}),
},
},
async handler(req, headers) {
const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null;
if (!alertsClient) {
return headers.response().code(404);
}
const start = req.payload.timeRange.min;
const end = req.payload.timeRange.max;
let alerts;
try {
alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger);
} catch (err) {
throw handleError(err, req);
}
return { alerts };
},
});
}