diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 536ab2ec29c80..5c92afa55109d 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -103,9 +103,7 @@ The number of requests per bucket is displayed when hovering over the graph, and [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] -Most of the requests fall into buckets on the left side of the graph, -with a long tail of smaller buckets to the right. -This is a typical distribution, and indicates most of our requests were served quickly - awesome! +This graph shows a typical distribution, and indicates most of our requests were served quickly - awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. When you select one of these buckets, you're presented with up to ten trace samples. diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc index 163579d5763b2..a25460a20eb50 100644 --- a/docs/canvas/canvas-elements.asciidoc +++ b/docs/canvas/canvas-elements.asciidoc @@ -138,7 +138,9 @@ To apply CSS overrides: . Next to *Element style*, click *+*, then select *CSS*. -. Enter the *CSS*. For example, to center the Markdown element, enter: +. Enter the *CSS*. ++ +For example, to center the Markdown element, enter: + [source,text] -------------------------------------------------- diff --git a/docs/canvas/canvas-present-workpad.asciidoc b/docs/canvas/canvas-present-workpad.asciidoc index 486686cd857b5..9cd4ecc9519e1 100644 --- a/docs/canvas/canvas-present-workpad.asciidoc +++ b/docs/canvas/canvas-present-workpad.asciidoc @@ -8,7 +8,7 @@ When you are ready to present your workpad, use and enable the presentation opti [[view-fullscreen-mode]] ==== View your workpad in fullscreen mode -In the upper left corner, click the *Enter fullscreen mode* icon. +Click the *Enter fullscreen mode* icon. [role="screenshot"] image::images/canvas-fullscreen.png[Fullscreen mode] @@ -19,7 +19,7 @@ image::images/canvas-fullscreen.png[Fullscreen mode] Automatically cycle through your workpads pages in fullscreen mode. -. In the upper left corner, click the *Control settings* icon. +. Click the *Control settings* icon. . Under *Change cycling interval*, select the interval you want to use. + diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index dbba12865b8ca..ee29926914ad6 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -10,7 +10,7 @@ When you've finished your workpad, you can share it outside of {kib}. Create a JSON file of your workpad that you can export outside of {kib}. -. From your workpad, click the *Share workpad* icon in the upper left corner. +. From your workpad, click the *Share workpad* icon. . Select *Download as JSON*. + @@ -27,7 +27,7 @@ If you have a license that supports the {report-features}, you can create a PDF For more information, refer to <>. -. From your workpad, click the *Share workpad* icon in the upper left corner, then select *PDF reports*. +. From your workpad, click the *Share workpad* icon, then select *PDF reports*. . Click *Generate PDF*. + @@ -42,7 +42,7 @@ If you have a license that supports the {report-features}, you can create a POST For more information, refer to <>. -. From your workpad, click the *Share workpad* icon in the upper left corner, then select *PDF reports*. +. From your workpad, click the *Share workpad* icon, then select *PDF reports*. . Click *Copy POST URL*. + @@ -55,7 +55,7 @@ image::images/canvas-create-URL.gif[Create POST URL] beta[] Canvas allows you to create _shareables_, which are workpads that you download and securely share on any website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar. -. From your workpad, click the *Share this workpad* icon in the upper left corner, then select *Share on a website*. +. From your workpad, click the *Share this workpad* icon, then select *Share on a website*. . On the *Share on a website* pane, follow the instructions. diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc index 2360e4c28ff15..4a87d4b84b783 100644 --- a/docs/dev-tools/searchprofiler/getting-started.asciidoc +++ b/docs/dev-tools/searchprofiler/getting-started.asciidoc @@ -3,10 +3,10 @@ === Getting Started The {searchprofiler} is automatically enabled in {kib}. Go to *Dev Tools > Search Profiler* -to get started. +to get started. {searchprofiler} displays the names of the indices searched, the shards in each index, -and how long it took for the query to complete. To try it out, replace the default `match_all` query +and how long it took for the query to complete. To try it out, replace the default `match_all` query with the query you want to profile and click *Profile*. The following example shows the results of profiling the `match_all` query. @@ -29,8 +29,8 @@ While the Cumulative Time metric is useful for comparing the performance of your indices and shards, it doesn't necessarily represent the actual physical query times. ==== -You can select the name of the shard and then click *View details* to see more profiling information, -including details about the query component(s) that ran on the shard, as well as the timing +You can select the name of the shard and then click *View details* to see more profiling information, +including details about the query component(s) that ran on the shard, as well as the timing breakdown of low-level Lucene methods. For more information, see {ref}/search-profile.html#profiling-queries[Profiling queries]. [float] @@ -40,10 +40,10 @@ By default, all queries executed by the {searchprofiler} are sent to `GET /_search`. It searches across your entire cluster (all indices, all types). If you need to query a specific index or type (or several), you can use the Index -and Type filters at the top left. +and Type filters. In the following example, the query is executed against the indices `test` and `kibana_1` and the type `my_type`. This is equivalent making a request to `GET /test,kibana_1/my_type/_search`. [role="screenshot"] -image::dev-tools/searchprofiler/images/filter.png["Filtering by index and type"] \ No newline at end of file +image::dev-tools/searchprofiler/images/filter.png["Filtering by index and type"] diff --git a/docs/discover/document-data.asciidoc b/docs/discover/document-data.asciidoc index 6e9218d66c115..477c2ec90e95c 100644 --- a/docs/discover/document-data.asciidoc +++ b/docs/discover/document-data.asciidoc @@ -26,7 +26,7 @@ and click image:images/sort-icon.png[]. The first click sorts by ascending order, the second click sorts by descending order, and the third click removes the field from the sorted fields. -Move a field column:: Hover over the column header and click the move left (<<) or move right icon (>>). +Move a field column:: Hover over the column header and click the (<<) or (>>) icons. Remove a field column :: Hover over the list of *Specified fields* and then click *remove*. Or, use the (x) control in the column header. diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index a16343aa4850a..acd4d6d908fd4 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -180,5 +180,5 @@ The map now looks like this: image::images/tutorial-visualize-map-2.png[] . Navigate the map by clicking and dragging. Use the controls -on the left to zoom the map and set filters. +to zoom the map and set filters. . *Save* this map with the name `Map Example`. diff --git a/docs/infrastructure/view-metrics.asciidoc b/docs/infrastructure/view-metrics.asciidoc index bbb981acc3ad6..1bd64dde76ee1 100644 --- a/docs/infrastructure/view-metrics.asciidoc +++ b/docs/infrastructure/view-metrics.asciidoc @@ -30,11 +30,3 @@ For complete control over the start and end times, click the start time or end t === Refresh the metrics You can click *Refresh* to manually refresh the metrics. - -[float] -[[infra-view-go-to-chart]] -=== Go to a specific chart - -The charts available for this component are shown in a list on the left of the page. Click a chart in the list to quickly go to that chart. - - diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index 72accdb5fe2aa..a7ed4e942f3f6 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -15,8 +15,7 @@ already activated a trial for 6.0, you cannot start a new trial until 7.0. You can, however, contact `info@elastic.co` to request an extended trial license. -When you activate a new license level, new features appear in the left sidebar -of the *Management* page. +When you activate a new license level, new features appear in *Management*. [role="screenshot"] image::images/management-license.png[] diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index fb5ef670692dc..990af3a018b1f 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -9,10 +9,10 @@ the dashboards and saved objects that belong to that space. {kib} creates a default space for you. After you create your own spaces, you're asked to choose a space when you log in to Kibana. You can change your -current space at any time by using the menu in the upper left. +current space at any time by using the menu. [role="screenshot"] -image::spaces/images/change-space.png["Change current space"] +image::spaces/images/change-space.png["Change current space menu"] Kibana supports spaces in several ways. You can: diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index 490edb9d26338..a17e46c5b3542 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -93,7 +93,7 @@ In *Edit* mode, you can move, resize, customize, and delete panels to suit your * To resize a panel, click the resize control on the lower right and drag to the new dimensions. -* To toggle the use of margins and panel titles, use the *Options* menu in the upper left. +* To toggle the use of margins and panel titles, use the *Options* menu. * To delete a panel, open the panel menu and select *Delete from dashboard.* Deleting a panel from a dashboard does *not* delete the saved visualization or search. diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 7de7d73bf1664..4222ba40debb7 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -24,7 +24,7 @@ image::images/Discover-Start.png[Discover] === Set up your index pattern The first thing to do in *Discover* is to select an <>, which -defines the data you want to explore and visualize. The current index pattern is in the upper left. +defines the data you want to explore and visualize. If you haven't yet created an index pattern, you can add a <>, which has a pre-built index pattern. @@ -69,7 +69,7 @@ image::images/filter-field.png[height=317] The sortable documents table lists the documents that match your search. By default, the table includes columns for the time field and the document `_source`. -To zero in on a specific field, click *add* next to the field name in the left sidebar. +To zero in on a specific field, click *add* next to the field name. For example, if you add the `currency`, `customer_last_name`, and `day_of_week` fields, the document table includes columns for those three fields. diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index e3f61565453b5..35570ea7ca1dc 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -32,7 +32,7 @@ Lens supports the following aggregations: [[drag-drop]] === Drag and drop -The data panel in the left column shows the data fields for the selected time period. When +The panel shows the data fields for the selected time period. When you drag a field from the data panel, Lens highlights where you can drop that field. The first time you drag a data field, you'll see two places highlighted in green: @@ -57,7 +57,7 @@ Lens shows you fields based on the <> you have d {kib}, and the current time range. When you change the index pattern or time filter, the list of fields are updated. -To narrow the list of fields you see in the left panel, you can: +To narrow the list of fields, you can: * Enter the field name in *Search field names*. @@ -100,11 +100,7 @@ still allows you to make the change. Lens allows some customizations of the data for each visualization. -. Change the index pattern. - -.. In the left column, click the index pattern name. - -.. Select the new index pattern. +. Click the index pattern name, then select the new index pattern. + If there is a match, Lens displays the new data. All fields that do not match the index pattern are removed. @@ -147,7 +143,7 @@ Drag and drop your data onto the visualization builder pane. . On the *New Visualization* window, click *Lens*. -. In the left column, select the *kibana_sample_data_ecommerce* index. +. Select the *kibana_sample_data_ecommerce* index. . Click image:images/time-filter-calendar.png[], then click *Last 7 days*. The list of data fields are updated. diff --git a/package.json b/package.json index e807cd4d95198..4c5db5321c282 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.4.0", - "@elastic/request-crypto": "^1.0.2", + "@elastic/request-crypto": "1.1.2", "@elastic/ui-ace": "0.2.3", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_visual_regression.sh index 4fdd197147eac..c6fefd45b005d 100755 --- a/test/scripts/jenkins_visual_regression.sh +++ b/test/scripts/jenkins_visual_regression.sh @@ -12,7 +12,7 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1 echo " -> running visual regression tests from kibana directory" checks-reporter-with-killswitch "X-Pack visual regression tests" \ - yarn percy exec -t 500 \ + yarn percy exec -t 500 -- -- \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$installDir" \ diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index 73e92da3bad63..96521ccc8f787 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -14,7 +14,7 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1 echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" checks-reporter-with-killswitch "X-Pack visual regression tests" \ - yarn percy exec -t 500 \ + yarn percy exec -t 500 -- -- \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$installDir" \ diff --git a/typings/elastic__node_crypto.d.ts b/typings/elastic__node_crypto.d.ts deleted file mode 100644 index 8d4b47da96b73..0000000000000 --- a/typings/elastic__node_crypto.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -declare module '@elastic/node-crypto'; diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts index 3a4c7b71dcd03..9a4030f3eb214 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -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 diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js index 95c1af5549198..11fcef73a4b97 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js @@ -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 ( - { - scope.$evalAsync(() => { - kbnUrl.changePath(target); - }); - }} - /> - ); - }, + render: (message, alert) => ( + { + 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(); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx index d3cf4b463a2cc..258a5b68db372 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx @@ -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({ diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx index 5f5329bf7fff8..072a98b123452 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx @@ -142,7 +142,7 @@ export const AlertsStatus: React.FC = (props: AlertsStatusPro ); } - const allMigrated = kibanaAlerts.length >= NUMBER_OF_MIGRATED_ALERTS; + const allMigrated = kibanaAlerts.length === NUMBER_OF_MIGRATED_ALERTS; if (allMigrated) { if (setupModeEnabled) { return ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index d87ff98e79be0..8455fb8cf3088 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -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 = ( - - {nonLinkText} - {linkPart[1]} - - ); - } - } - - 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 ( @@ -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 = ( + + {message} +   + Please update your license + + ); + } } return ( - - -

{message}

- -

- -

-
-
- -
+ +

{message}

+
); }) : alerts.map((item, index) => ( diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js index 62cc985887e9f..7c065a78a8af9 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js @@ -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); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts index 6f415d7ee5ea9..0436f5d5bc843 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { cryptoFactory } from '../../../server/lib/crypto'; -import { CryptoFactory, Logger } from '../../../types'; +import { Logger } from '../../../types'; interface HasEncryptedHeaders { headers?: string; @@ -25,9 +25,16 @@ export const decryptJobHeaders = async < job: JobDocPayloadType; logger: Logger; }): Promise> => { - const crypto: CryptoFactory = cryptoFactory(encryptionKey); try { - const decryptedHeaders: Record = await crypto.decrypt(job.headers); + if (typeof job.headers !== 'string') { + throw new Error( + i18n.translate('xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage', { + defaultMessage: 'Job headers are missing', + }) + ); + } + const crypto = cryptoFactory(encryptionKey); + const decryptedHeaders = (await crypto.decrypt(job.headers)) as Record; return decryptedHeaders; } catch (err) { logger.error(err); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index d78d8a8a8010d..3a282eb0b2974 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -43,9 +43,18 @@ export const executeJobFactory: ExecuteJobFactory { - let decryptedHeaders; try { - decryptedHeaders = await crypto.decrypt(headers); + if (typeof headers !== 'string') { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage', + { + defaultMessage: 'Job headers are missing', + } + ) + ); + } + return await crypto.decrypt(headers); } catch (err) { logger.error(err); throw new Error( @@ -58,7 +67,6 @@ export const executeJobFactory: ExecuteJobFactory; const serializedEncryptedHeaders = job.headers; try { - decryptedHeaders = await crypto.decrypt(serializedEncryptedHeaders); + if (typeof serializedEncryptedHeaders !== 'string') { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage', + { + defaultMessage: 'Job headers are missing', + } + ) + ); + } + decryptedHeaders = (await crypto.decrypt(serializedEncryptedHeaders)) as Record< + string, + unknown + >; } catch (err) { jobLogger.error(err); throw new Error( diff --git a/x-pack/legacy/plugins/reporting/server/lib/crypto.ts b/x-pack/legacy/plugins/reporting/server/lib/crypto.ts index 97876529ecfa7..0394c8ed1fbad 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/crypto.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/crypto.ts @@ -6,6 +6,10 @@ import nodeCrypto from '@elastic/node-crypto'; -export function cryptoFactory(encryptionKey: string | undefined) { +export function cryptoFactory(encryptionKey?: string) { + if (typeof encryptionKey !== 'string') { + throw new Error('Encryption Key required.'); + } + return nodeCrypto({ encryptionKey }); } diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 09d53278941c9..7334a859005e0 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -116,10 +116,6 @@ export interface ConditionalHeadersConditions { basePath: string; } -export interface CryptoFactory { - decrypt: (headers?: string) => any; -} - export interface IndexPatternSavedObject { attributes: { fieldFormatMap: string; diff --git a/x-pack/package.json b/x-pack/package.json index 24b23256bf18e..b2ec4c3dc3f6f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -185,7 +185,7 @@ "@elastic/eui": "21.0.1", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.2.0", - "@elastic/node-crypto": "^1.0.0", + "@elastic/node-crypto": "1.1.1", "@elastic/numeral": "2.4.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts index e1e1a8224aa7b..be33238a26ee7 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts @@ -19,9 +19,10 @@ beforeEach(() => { mockAuditLogger = encryptedSavedObjectsAuditLoggerMock.create(); // Call actual `@elastic/node-crypto` by default, but allow to override implementation in tests. - jest - .requireMock('@elastic/node-crypto') - .mockImplementation((...args: any[]) => jest.requireActual('@elastic/node-crypto')(...args)); + jest.requireMock('@elastic/node-crypto').mockImplementation((...args: any[]) => { + const { default: nodeCrypto } = jest.requireActual('@elastic/node-crypto'); + return nodeCrypto(...args); + }); service = new EncryptedSavedObjectsService( 'encryption-key-abc', diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 94c1684529577..eea2b12354d9b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import nodeCrypto from '@elastic/node-crypto'; +import nodeCrypto, { Crypto } from '@elastic/node-crypto'; import stringify from 'json-stable-stringify'; import typeDetect from 'type-detect'; import { Logger } from 'src/core/server'; @@ -49,10 +48,7 @@ export function descriptorToArray(descriptor: SavedObjectDescriptor) { * attributes. */ export class EncryptedSavedObjectsService { - private readonly crypto: Readonly<{ - encrypt(valueToEncrypt: T, aad?: string): Promise; - decrypt(valueToDecrypt: string, aad?: string): Promise; - }>; + private readonly crypto: Readonly; /** * Map of all registered saved object types where the `key` is saved object type and the `value` @@ -229,10 +225,10 @@ export class EncryptedSavedObjectsService { } try { - decryptedAttributes[attributeName] = await this.crypto.decrypt( + decryptedAttributes[attributeName] = (await this.crypto.decrypt( attributeValue, encryptionAAD - ); + )) as string; } catch (err) { this.logger.error(`Failed to decrypt "${attributeName}" attribute: ${err.message || err}`); this.audit.decryptAttributeFailure(attributeName, descriptor); diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 3a4c7b71dcd03..9a4030f3eb214 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -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 diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts deleted file mode 100644 index 6a9ca88437347..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts +++ /dev/null @@ -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 { - 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); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts deleted file mode 100644 index 9a5805b8af7ce..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts +++ /dev/null @@ -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, - 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 { - 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; - }, - }; -}; diff --git a/x-pack/plugins/monitoring/server/alerts/enums.ts b/x-pack/plugins/monitoring/server/alerts/enums.ts deleted file mode 100644 index ccff588743af1..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/enums.ts +++ /dev/null @@ -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', -} diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts index 92047e300bc1f..0773af6e7f070 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -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'; - -jest.mock('../lib/alerts/license_expiration.lib', () => ({ - executeActions: jest.fn(), - getUiMessage: jest.fn(), -})); - -jest.mock('../lib/alerts/get_prepared_alert', () => ({ - getPreparedAlert: jest.fn(() => { - return { - emailAddress: 'foo@foo.com', - }; - }), -})); +import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; + +function fillLicense(license: any, clusterUuid?: string) { + return { + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + }; +} + +const clusterUuid = 'a4545jhjb'; +const params: AlertParams = { + dateFormat: 'YYYY', + timezone: 'UTC', +}; interface MockServices { callCluster: jest.Mock; @@ -38,169 +49,428 @@ interface MockServices { savedObjectsClient: jest.Mock; } -describe('getLicenseExpiration', () => { - const services: MockServices | AlertServices = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), - }; - - const params: AlertCommonParams = { - dateFormat: 'YYYY', - timezone: 'UTC', - }; +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, + }, + params: {}, + state: {}, + spaceId: '', + name: '', + tags: [], + previousStartedAt: null, + createdBy: null, + updatedBy: null, +}; +describe('getLicenseExpiration', () => { 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 { - 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 => { + 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 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 = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + 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 result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; + + 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(executeActions).toHaveBeenCalledWith( - undefined, - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress + 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 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 = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + 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 = await setupAlert(license, 100); - const newState = result[clusterUuid] as AlertLicensePerClusterState; + + 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(executeActions).toHaveBeenCalledWith( - undefined, - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress, - true + 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 = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + 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 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 = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + 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 result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; + + 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(executeActions).toHaveBeenCalledWith( - undefined, - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress - ); + expect(scheduleActions.mock.calls.length).toBe(1); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts index 2e5356150086b..93397ff3641ae 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts @@ -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 { + 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 { + async executor({ + services, + params, + state, + }: LicenseExpirationAlertExecutorOptions): Promise { 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); + + // 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); - if (!preparedAlert) { + // 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('dateFormat'); + const timezone: string = await uiSettings.get('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(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; diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts index b689d008b51a7..ff47d6f2ad4dc 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts @@ -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; + clusterName: 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 AlertState { + [clusterUuid: string]: AlertClusterState; } -export interface AlertLicensePerClusterState extends AlertCommonPerClusterState { - expiredCheckDateMS: number; +export interface AlertClusterState { + expiredCheckDateMS: number | Moment; + ui: AlertClusterUiState; } -export interface AlertCommonPerClusterUiState { +export interface AlertClusterUiState { isFiring: boolean; severity: number; - message: AlertCommonPerClusterMessage | null; + message: string | 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 { +export interface AlertCluster { clusterUuid: string; - clusterName: string; } -export interface AlertCommonExecutorOptions extends AlertExecutorOptions { - state: AlertCommonState; +export interface LicenseExpirationAlertExecutorOptions extends AlertExecutorOptions { + state: AlertState; } -export interface AlertCommonParams { +export interface AlertParams { dateFormat: string; timezone: string; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts deleted file mode 100644 index 81e375734cc50..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts +++ /dev/null @@ -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(); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts deleted file mode 100644 index ae66d603507ca..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts +++ /dev/null @@ -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, - ], - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts deleted file mode 100644 index 642ae3c39a027..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts +++ /dev/null @@ -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, - }, - ]); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts deleted file mode 100644 index 66ea30d5f2e96..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts +++ /dev/null @@ -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 { - 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(response, 'hits.hits', []).map((hit: any) => { - return { - state: get(hit, '_source.cluster_state.status'), - clusterUuid: get(hit, '_source.cluster_uuid'), - }; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts index 7a9b61f37707b..78eb9773df15f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -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, - }, - }, - ], - }, - })); - 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, - }, - }, - }, + aggregations: { + clusters: { + buckets: [ + { + key: 'clusterA', }, - }, - ], + ], + }, }, })); 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 () => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index d1513ac16fb15..8ef7339618a2c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -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 { +interface AggregationResult { + key: string; +} + +export async function fetchClusters(callCluster: any, index: string): Promise { 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, + })); } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts index 9dcb4ffb82a5f..dd6c074e68b1f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -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, + }, + ]); + }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 5b05c907e796e..31a68e8aa9c3e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -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 { 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(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; }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts deleted file mode 100644 index a3bcb61afacd6..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ /dev/null @@ -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([]); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index bf6ee965d3b2f..9f7c1d5a994d2 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -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 { + // 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) { - return resolve(false); - } - - if (alerts.total !== 1) { - log.warn(`Found more than one alert for type ${type} which is unexpected.`); - } + 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 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.`); + const response = await callCluster('search', params); + const state = get(response, 'hits.hits[0]._source.task.state', '{}'); + const clusterState: AlertClusterState = get( + 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); } - - 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); }) ) ); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts deleted file mode 100644 index 1840a2026a753..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts deleted file mode 100644 index 83a9e26e4c589..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts +++ /dev/null @@ -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 -): Promise { - if (!monitoringCluster) { - return services.callCluster; - } - - return monitoringCluster.callAsInternalUser; -} - -export async function getPreparedAlert( - alertType: string, - getUiSettingsService: () => Promise, - monitoringCluster: ICustomClusterClient, - logger: Logger, - ccsEnabled: boolean, - services: Pick, - dataFetcher: ( - callCluster: CallCluster, - clusters: AlertCommonCluster[], - esIndexPattern: string - ) => Promise -): Promise { - 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('dateFormat'); - const timezone: string = await uiSettings.get('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, - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts index 6c0301b6cc347..1a2eb1e44be84 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts @@ -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.`); }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts index a590021a2f29b..41b68d69bbd25 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -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, - ], - }; } diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 1bddede52207b..c5091c36c3bbe 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -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, diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 784226dca66fe..24d8bcaa4397c 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -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 diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js index d5a43d32f600a..56922bd8e87e2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js @@ -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 }; - }, - }); } diff --git a/x-pack/typings/elastic__node_crypto.d.ts b/x-pack/typings/elastic__node_crypto.d.ts deleted file mode 100644 index 463b662d5b207..0000000000000 --- a/x-pack/typings/elastic__node_crypto.d.ts +++ /dev/null @@ -1,7 +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. - */ - -declare module '@elastic/node-crypto'; diff --git a/yarn.lock b/yarn.lock index d9edb55a32039..77ab69c715573 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1343,27 +1343,22 @@ resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.2.0.tgz#d0a85aa248bdc14dca44e1f9430c0b670f65e489" integrity sha512-QkmRNpEY4Dy6eqwDimR5X9leMgdPFjdANmpEIwEW1XVUG2U4YtB2BXhDxsnMmNTUrJUjtnjnwgwBUyg0pU0FTg== -"@elastic/node-crypto@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-0.1.2.tgz#c18ac282f635e88f041cc1555d806e492ca8f3b1" - integrity sha1-wYrCgvY16I8EHMFVXYBuSSyo87E= - -"@elastic/node-crypto@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.0.0.tgz#4d325df333fe1319556bb4d54214098ada1171d4" - integrity sha512-bbjbEyILPRTRt0xnda18OttLtlkJBPuXx3CjISUSn9jhWqHoFMzfOaZ73D5jxZE2SaFZUrJYfPpqXP6qqPufAQ== +"@elastic/node-crypto@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.1.1.tgz#619b70322c9cce4a7ee5fbf8f678b1baa7f06095" + integrity sha512-F6tIk8Txdqjg8Siv60iAvXzO9ZdQI87K3sS/fh5xd2XaWK+T5ZfqeTvsT7srwG6fr6uCBfuQEJV1KBBl+JpLZA== "@elastic/numeral@2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.4.0.tgz#883197b7f4bf3c2dd994f53b274769ddfa2bf79a" integrity sha512-uGBKGCNghTgUZPHClji/00v+AKt5nidPTGOIbcT+lbTPVxNB6QPpPLGWtXyrg3QZAxobPM/LAZB1mAqtJeq44Q== -"@elastic/request-crypto@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@elastic/request-crypto/-/request-crypto-1.0.2.tgz#bf27bf009227166f3eeb2b5193a108752335ebd3" - integrity sha512-8FtGYl7LebhmJmEDWiGn3MorvNiGWSYPqhvgRlKXjNakEuLoPBBe0DHxbwLkj08CMLWczXcO2ixqBPY7fEhJpA== +"@elastic/request-crypto@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@elastic/request-crypto/-/request-crypto-1.1.2.tgz#2e323550f546f6286994126d462a9ea480a3bfb1" + integrity sha512-i73wjj1Qi8dGJIy170Z8xyJ760mFNjTbdmcp/nEczqWD0miNW6I5wZ5MNrv7M6CXn2m1wMXiT6qzDYd93Hv1Dw== dependencies: - "@elastic/node-crypto" "^0.1.2" + "@elastic/node-crypto" "1.1.1" "@types/node-jose" "1.1.0" node-jose "1.1.0"