diff --git a/cypress/integration/cluster_metrics_monitor_spec.js b/cypress/integration/cluster_metrics_monitor_spec.js index 897766dfa..e66145e7c 100644 --- a/cypress/integration/cluster_metrics_monitor_spec.js +++ b/cypress/integration/cluster_metrics_monitor_spec.js @@ -15,7 +15,7 @@ const SAMPLE_DESTINATION = 'sample_destination'; const addClusterMetricsTrigger = (triggerName, triggerIndex, actionName, isEdit, source) => { // Click 'Add trigger' button - cy.contains('Add trigger', { timeout: 20000 }).click({ force: true }); + cy.contains('Add trigger', { timeout: 30000 }).click({ force: true }); if (isEdit === true) { // TODO: Passing button props in EUI accordion was added in newer versions (31.7.0+). @@ -37,7 +37,7 @@ const addClusterMetricsTrigger = (triggerName, triggerIndex, actionName, isEdit, force: true, parseSpecialCharSequences: false, delay: 5, - timeout: 20000, + timeout: 30000, }) .trigger('blur', { force: true }); }); @@ -76,7 +76,7 @@ describe('ClusterMetricsMonitor', () => { cy.visit(`${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors`); // Common text to wait for to confirm page loaded, give up to 20 seconds for initial load - cy.contains('Create monitor', { timeout: 20000 }); + cy.contains('Create monitor', { timeout: 30000 }); }); describe('can be created', () => { @@ -87,7 +87,7 @@ describe('ClusterMetricsMonitor', () => { it('for the Cluster Health API', () => { // Go to create monitor page - cy.contains('Create monitor', { timeout: 20000 }).click({ force: true }); + cy.contains('Create monitor', { timeout: 30000 }).click({ force: true }); // Select ClusterMetrics radio card cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true }); @@ -141,7 +141,7 @@ describe('ClusterMetricsMonitor', () => { it('for the Nodes Stats API', () => { // Go to create monitor page - cy.contains('Create monitor', { timeout: 20000 }).click({ force: true }); + cy.contains('Create monitor', { timeout: 30000 }).click({ force: true }); // Select ClusterMetrics radio card cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true }); @@ -202,7 +202,7 @@ describe('ClusterMetricsMonitor', () => { it('for the CAT Snapshots API', () => { // Go to create monitor page - cy.contains('Create monitor', { timeout: 20000 }).click({ force: true }); + cy.contains('Create monitor', { timeout: 30000 }).click({ force: true }); // Select ClusterMetrics radio card cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true }); @@ -228,7 +228,7 @@ describe('ClusterMetricsMonitor', () => { // Begin monitor creation // Go to create monitor page - cy.contains('Create monitor', { timeout: 20000 }).click({ force: true }); + cy.contains('Create monitor', { timeout: 30000 }).click({ force: true }); // Select ClusterMetrics radio card cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true }); @@ -335,7 +335,7 @@ describe('ClusterMetricsMonitor', () => { cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').contains('Cluster settings'); // Confirm there are 0 triggers defined - cy.contains('Triggers (0)', { timeout: 20000 }); + cy.contains('Triggers (0)', { timeout: 30000 }); }); }); }); diff --git a/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap b/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap index 4d1dfa1b6..3c2c05e33 100644 --- a/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap +++ b/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap @@ -19,6 +19,7 @@ exports[`AddAlertingMonitor renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -60,6 +61,7 @@ exports[`AddAlertingMonitor renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", diff --git a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js index e95a89d9f..a6af885c6 100644 --- a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js +++ b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js @@ -24,6 +24,7 @@ import { getTime } from '../../../../pages/MonitorDetails/components/MonitorOver import { PLUGIN_NAME } from '../../../../../utils/constants'; import { ALERT_STATE, + DEFAULT_EMPTY_DATA, MONITOR_GROUP_BY, MONITOR_INPUT_DETECTOR_ID, MONITOR_TYPE, @@ -35,8 +36,6 @@ import { UNITS_OF_TIME } from '../../../../pages/CreateMonitor/components/Monito import { DEFAULT_WHERE_EXPRESSION_TEXT } from '../../../../pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers'; import { acknowledgeAlerts, backendErrorNotification } from '../../../../utils/helpers'; import { - displayAcknowledgedAlertsToast, - filterActiveAlerts, getQueryObjectFromState, getURLQueryParams, insertGroupByColumn, @@ -54,6 +53,11 @@ import { TABLE_TAB_IDS, } from '../../../../pages/Dashboard/components/FindingsDashboard/findingsUtils'; import FindingsDashboard from '../../../../pages/Dashboard/containers/FindingsDashboard'; +import { CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN } from '../../../../pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants'; +import { + getDataSources, + getLocalClusterName, +} from '../../../../pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers'; export const DEFAULT_NUM_FLYOUT_ROWS = 10; @@ -73,6 +77,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { alerts: [], alertState: alertState, loading: true, + localClusterName: undefined, monitor: monitor, monitorIds: [monitor_id], monitorType: monitorType, @@ -103,6 +108,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { alertState, monitorIds ); + this.getLocalClusterName(); } componentDidUpdate(prevProps, prevState) { @@ -141,7 +147,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { getMultipleGraphConditions = (trigger) => { let conditions = _.get(trigger, 'condition.script.source'); if (_.isEmpty(conditions)) { - return '-'; + return DEFAULT_EMPTY_DATA; } else { conditions = conditions.replaceAll(' && ', '&AND&'); conditions = conditions.replaceAll(' || ', '&OR&'); @@ -150,6 +156,12 @@ export default class AlertsDashboardFlyoutComponent extends Component { } }; + getLocalClusterName = async () => { + this.setState({ + localClusterName: await getLocalClusterName(this.props.httpClient), + }); + }; + getSeverityText = (severity) => { return _.get(_.find(SEVERITY_OPTIONS, { value: severity }), 'text'); }; @@ -319,6 +331,10 @@ export default class AlertsDashboardFlyoutComponent extends Component { case MONITOR_TYPE.BUCKET_LEVEL: columns = insertGroupByColumn(groupBy); break; + case MONITOR_TYPE.CLUSTER_METRICS: + columns = _.cloneDeep(queryColumns); + columns.push(CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN); + break; case MONITOR_TYPE.DOC_LEVEL: columns = _.cloneDeep(queryColumns); columns.splice( @@ -495,7 +511,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { triggerID, trigger_name, } = this.props; - const { loading, monitor, monitorType, tabContent } = this.state; + const { loading, localClusterName, monitor, monitorType, tabContent } = this.state; const searchType = _.get(monitor, 'ui_metadata.search.searchType', SEARCH_TYPE.GRAPH); const triggerType = this.getTriggerType(monitorType); @@ -511,7 +527,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { searchType === SEARCH_TYPE.GRAPH && (monitorType === MONITOR_TYPE.BUCKET_LEVEL || monitorType === MONITOR_TYPE.DOC_LEVEL) ? this.getMultipleGraphConditions(trigger) - : _.get(trigger, 'condition.script.source', '-'); + : _.get(trigger, 'condition.script.source', DEFAULT_EMPTY_DATA); let displayMultipleConditions; switch (monitorType) { @@ -526,7 +542,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { const filters = monitorType === MONITOR_TYPE.BUCKET_LEVEL && searchType === SEARCH_TYPE.GRAPH ? this.getBucketLevelGraphFilter(trigger) - : '-'; + : DEFAULT_EMPTY_DATA; const bucketValue = _.get(monitor, 'ui_metadata.search.bucketValue'); let bucketUnitOfTime = _.get(monitor, 'ui_metadata.search.bucketUnitOfTime'); @@ -536,7 +552,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { const timeRangeForLast = bucketValue !== undefined && !_.isEmpty(bucketUnitOfTime) ? `${bucketValue} ${bucketUnitOfTime}` - : '-'; + : DEFAULT_EMPTY_DATA; let displayTableTabs; switch (monitorType) { @@ -551,6 +567,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { monitorType === MONITOR_TYPE.COMPOSITE_LEVEL ? '?type=workflow' : '' }`; + const dataSources = getDataSources(monitor, localClusterName).join('\n'); return (
@@ -566,7 +583,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { Severity -

{this.getSeverityText(severity) || severity || '-'}

+

{this.getSeverityText(severity) || severity || DEFAULT_EMPTY_DATA}

@@ -599,6 +616,12 @@ export default class AlertsDashboardFlyoutComponent extends Component {

+ + + Monitor data sources +

{dataSources}

+
+
@@ -651,7 +674,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { ? 'Loading groups...' : !_.isEmpty(groupBy) ? _.join(_.orderBy(groupBy), ', ') - : '-'} + : DEFAULT_EMPTY_DATA}

diff --git a/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap b/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap index a0549545e..0b87d9708 100644 --- a/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap +++ b/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap @@ -76,6 +76,24 @@ exports[`AlertsDashboardFlyoutComponent renders 1`] = `

+ + + + Monitor data sources + +

+ - +

+
+
{}, + dataSources = [], + localClusterName = '', + monitorType = MONITOR_TYPE.QUERY_LEVEL, +}) => { + const columns = [ + { + field: 'cluster', + name: 'Data connection', + sortable: true, + truncateText: true, + }, + ]; + switch (monitorType) { + case MONITOR_TYPE.CLUSTER_METRICS: + // Cluster metrics monitors do not use indexes as data sources; excluding that column. + break; + default: + columns.push({ + field: 'index', + name: 'Index', + sortable: true, + truncateText: true, + }); + } + + const indexItems = dataSources.map((dataSource = '', int) => { + const item = { id: int }; + switch (monitorType) { + case MONITOR_TYPE.CLUSTER_METRICS: + item.cluster = + dataSource === localClusterName ? `${dataSource} (Local)` : `${dataSource} (Remote)`; + break; + default: + const shouldSplit = dataSource.includes(':'); + const splitIndex = dataSource.split(':'); + let clusterName = shouldSplit ? splitIndex[0] : localClusterName; + clusterName = + clusterName === localClusterName ? `${clusterName} (Local)` : `${clusterName} (Remote)`; + const indexName = shouldSplit ? splitIndex[1] : dataSource; + item.cluster = clusterName; + item.index = indexName; + } + return item; + }); + + return { + flyoutProps: { + 'aria-labelledby': 'dataSourcesFlyout', + size: 'm', + hideCloseButton: true, + 'data-test-subj': `dataSourcesFlyout`, + }, + headerProps: { hasBorder: true }, + header: ( + + + +

{`Data sources`}

+
+
+ + + +
+ ), + footerProps: { style: { backgroundColor: '#F5F7FA' } }, + body: ( + item.id} + columns={columns} + pagination={true} + isSelectable={false} + hasActions={false} + noItemsMessage={'No data sources configured for this monitor.'} + data-test-subj={'dataSourcesFlyout_table'} + /> + ), + }; +}; + +export default dataSources; diff --git a/public/components/Flyout/flyouts/index.js b/public/components/Flyout/flyouts/index.js index 90b7aff9f..8ab6c60c2 100644 --- a/public/components/Flyout/flyouts/index.js +++ b/public/components/Flyout/flyouts/index.js @@ -7,12 +7,14 @@ import message from './message'; import messageFrequency from './messageFrequency'; import triggerCondition from './triggerCondition'; import alertsDashboard from './alertsDashboard'; +import dataSources from './dataSources'; const Flyouts = { messageFrequency, message, triggerCondition, alertsDashboard, + dataSources, }; export default Flyouts; diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js index fa66e696e..81c976c54 100644 --- a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import _ from 'lodash'; +import { DEFAULT_EMPTY_DATA } from '../../../../../utils/constants'; + export const URL_DEFAULT_PREFIX = 'http://localhost:9200'; export const API_PATH_REQUIRED_PLACEHOLDER_TEXT = 'Select an API.'; export const EMPTY_PATH_PARAMS_TEXT = 'Enter remaining path components and path parameters'; @@ -31,6 +34,14 @@ export const DEFAULT_CLUSTER_METRICS_SCRIPT = { source: 'ctx.results[0] != null', }; +export const CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN = { + field: 'clusters', + name: 'Triggered clusters', + sortable: true, + truncateText: true, + render: (clusters = [DEFAULT_EMPTY_DATA]) => _.sortBy(clusters).join(', '), +}; + export const API_TYPES = { CLUSTER_HEALTH: { type: 'CLUSTER_HEALTH', diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js index 018614b7f..69ae2ab9b 100644 --- a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js +++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js @@ -68,6 +68,7 @@ describe('clusterMetricsMonitorHelpers', () => { path: path, path_params: '', url: `http://localhost:9200/${path}`, + clusters: [], }, }; expect(buildClusterMetricsRequest(values)).toEqual(expectedResult.uri); @@ -90,6 +91,7 @@ describe('clusterMetricsMonitorHelpers', () => { path: path, path_params: '/' + pathParams, url: `http://localhost:9200/${path}/${pathParams}`, + clusters: [], }, }; expect(buildClusterMetricsRequest(values)).toEqual(expectedResult.uri); diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/components/ExperimentalBanner.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/components/ExperimentalBanner.js new file mode 100644 index 000000000..e5f58fb90 --- /dev/null +++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/components/ExperimentalBanner.js @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; + +export const REMOTE_MONITORING_ENABLED_SETTING_PATH = 'plugins.alerting.remote_monitoring_enabled'; + +export const ExperimentalBanner = () => { + return ( + <> + +

+ The feature is experimental and should not be used in a production environment. Any index + patterns, visualization, and observability panels will be impacted if the feature is + deactivated. For more information see  + + Alerting documentation + + . To leave feedback, visit  + + forum.opensearch.org + +

+
+ + + ); +}; diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/CrossClusterConfiguration.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/CrossClusterConfiguration.js new file mode 100644 index 000000000..14a2067cd --- /dev/null +++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/CrossClusterConfiguration.js @@ -0,0 +1,397 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import _ from 'lodash'; +import { EuiHealth, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormikComboBox } from '../../../../../components/FormControls'; +import { MONITOR_TYPE } from '../../../../../utils/constants'; +import { connect } from 'formik'; +import { validateIndex } from '../../../../../utils/validate'; +import { ExperimentalBanner } from '../components/ExperimentalBanner'; + +export const CROSS_CLUSTER_SETUP_LINK = + 'https://opensearch.org/docs/latest/security/access-control/cross-cluster-search/'; + +export const HEALTH_TO_COLOR = { + green: 'success', + yellow: 'warning', + red: 'danger', + undefined: 'subdued', +}; + +export const GENERIC_LOCAL_CLUSTER_KEY = '_localCluster'; +export class CrossClusterConfiguration extends Component { + constructor(props) { + super(props); + this.state = { + loadedInitialValues: false, + loading: true, + localClusterName: '', + clusterCount: 0, + clusterOptions: [], + selectedClusters: [], + indexOptions: [], + selectedIndexes: [], + }; + } + + componentDidMount() { + this.getIndexes(); + } + + componentDidUpdate(prevProps, prevState) { + const { loadedInitialValues, selectedClusters } = this.state; + if (prevState.selectedClusters !== selectedClusters && loadedInitialValues) this.getIndexes(); + } + + async getIndexes() { + const { httpClient } = this.props; + const { loadedInitialValues, selectedClusters } = this.state; + this.setState({ loading: true }); + try { + const indexes = selectedClusters.map((cluster) => + cluster.hub_cluster ? '*' : `${cluster.cluster}:*` + ); + const query = { + indexes: indexes.length === 0 ? '*,*:*' : indexes.join(','), + include_mappings: !loadedInitialValues, + }; + const response = await httpClient.get(`../api/alerting/remote/indexes`, { query: query }); + if (response.ok) { + this.parseOptions(response.resp); + } else { + console.log('Error getting clusters:', response); + } + } catch (e) { + console.error(e); + } + this.setState({ loading: false }); + } + + parseOptions = (clusterInfos = {}) => { + const { + formik: { values }, + } = this.props; + const { loadedInitialValues } = this.state; + const clusterOptions = []; + const categorizedClusterOptions = { + Local: [], + Remote: [], + }; + const categorizedIndexOptions = {}; + const selectedClusters = this.state.selectedClusters; + const selectedIndexes = this.state.selectedIndexes; + let localClusterName = ''; + + // Parse the selected clusters and indexes when editing a monitor. + const indexes = {}; + if (!loadedInitialValues && !selectedIndexes.length && (values.index || []).length) { + // In 'values', 'index' consists of an array of '{ label: index-name }' objects. + values?.index.forEach(({ label }) => { + if (label.includes(':')) { + // Splits the index from 'cluster-name:index-name' format to an array with the cluster + // name as entry 0, and index name as entry 1. + const clusterName = label.split(':')[0]; + const indexName = label.split(':')[1]; + if (!indexes[clusterName]) indexes[clusterName] = []; + indexes[clusterName].push(indexName); + } else { + // Indexes in `index-name` format indicate the local cluster + if (!indexes[GENERIC_LOCAL_CLUSTER_KEY]) indexes[GENERIC_LOCAL_CLUSTER_KEY] = []; + indexes[GENERIC_LOCAL_CLUSTER_KEY].push(label); + } + }); + } + + const getClusterOptionLabel = (clusterInfo) => + `${clusterInfo.cluster} ${clusterInfo.hub_cluster ? '(Local)' : '(Remote)'}`; + + Object.entries(clusterInfos).forEach(([clusterName, clusterInfo]) => { + const clusterLabel = getClusterOptionLabel(clusterInfo); + const clusterOption = { + label: clusterLabel, + cluster: clusterInfo.cluster, + health: clusterInfo.health, + hub_cluster: clusterInfo.hub_cluster, + latency: clusterInfo.latency, + }; + clusterOptions.push(clusterOption); + if (clusterOption.hub_cluster) { + localClusterName = clusterOption.cluster; + categorizedClusterOptions.Local.push(clusterOption); + + // To simplify iterations, consolidate any indexes listed under GENERIC_LOCAL_CLUSTER_KEY + // with the local cluster, then delete the GENERIC_LOCAL_CLUSTER_KEY entry. + indexes[localClusterName] = (indexes[localClusterName] || []).concat( + indexes[GENERIC_LOCAL_CLUSTER_KEY] || [] + ); + delete indexes[GENERIC_LOCAL_CLUSTER_KEY]; + } else { + categorizedClusterOptions.Remote.push(clusterOption); + } + + if (!loadedInitialValues) { + // Parse the selected clusters when editing a monitor. + switch (values.monitor_type) { + case MONITOR_TYPE.CLUSTER_METRICS: + if ((values.clusterNames || []).includes(clusterName)) + selectedClusters.push(clusterOption); + break; + default: + if (Object.keys(indexes).includes(clusterName)) selectedClusters.push(clusterOption); + } + + // Select the local cluster by default if there are no other selected clusters. + if (!selectedClusters.length && clusterInfo.hub_cluster) { + selectedClusters.push(clusterOption); + this.setState({ selectedClusters: selectedClusters }); + } + } + + // Only display indexes for the selected clusters + if (selectedClusters.some((option) => option.cluster === clusterName)) { + const clusterIndexOptions = + clusterInfo.indexes === undefined + ? [] + : Object.entries(clusterInfo.indexes).map(([_, indexInfo]) => { + const indexOption = { + label: indexInfo.name, + health: indexInfo.health, + index: indexInfo.name, + cluster: clusterInfo.cluster, + value: + clusterInfo.cluster === undefined || clusterInfo.hub_cluster + ? indexInfo.name + : `${clusterInfo.cluster}:${indexInfo.name}`, + }; + + // Parse the selected indexes when editing a monitor. + if ( + !loadedInitialValues && + (indexes[clusterName] || []).includes(indexOption.index) + ) + selectedIndexes.push(indexOption); + return indexOption; + }); + + if (!categorizedIndexOptions[clusterInfo.cluster]) + categorizedIndexOptions[clusterInfo.cluster] = { label: clusterLabel, options: [] }; + + categorizedIndexOptions[clusterInfo.cluster].options = _.orderBy( + clusterIndexOptions, + ['index'], + ['asc'] + ); + } + }); + + categorizedClusterOptions.Remote = _.orderBy( + categorizedClusterOptions.Remote, + ['hub_cluster', 'cluster'], + [`desc`, 'asc'] + ); + + let outputState = {}; + if (!loadedInitialValues) { + // Create generic indexOptions for any pre-selected indexes that have not yet been added to selectedIndexes. + Object.entries(indexes).forEach(([clusterName, indexList = []]) => { + indexList.forEach((index) => { + const includesIndex = (categorizedIndexOptions[clusterName].options || []).some( + (option) => option.index === index + ); + if (!includesIndex) { + const isLocalCluster = clusterName === localClusterName; + const newOption = { + label: index, + health: undefined, + index: index, + cluster: clusterName, + value: isLocalCluster ? index : `${clusterName}:${index}`, + }; + selectedIndexes.push(newOption); + } + }); + }); + + outputState = { + clusterCount: clusterOptions.length, + clusterOptions: Object.entries(categorizedClusterOptions).map(([category, clusters]) => ({ + label: category, + options: clusters, + })), + loadedInitialValues: true, + localClusterName: localClusterName, + selectedClusters: selectedClusters, + selectedIndexes: this.renderSelectedClusterIndexesOptions(selectedIndexes), + }; + } + + let indexOptions = Object.entries(categorizedIndexOptions).map( + ([_, clusterIndexOptions]) => clusterIndexOptions + ); + indexOptions = _.orderBy(indexOptions, ['label'], ['asc']); + this.setState({ + ...outputState, + indexOptions: indexOptions, + }); + }; + + renderClusterOption = (option) => { + const { label, health } = option; + return {label}; + }; + + onClustersChange = (options = [], field, form) => { + const { clusterOptions, selectedClusters, selectedIndexes } = this.state; + // If no clusters are selected, select the local cluster. + if (!options.length) { + const localClusterOption = clusterOptions.find((category) => category.label === 'Local') + ?.options[0]; + options.push(localClusterOption); + } + + // Remove index selections for cluster that are no longer selected. + if (options.length && options.some((option) => !selectedClusters.includes(option))) { + const clusterNames = options.map((clusterOption) => clusterOption.cluster); + const matchingIndexes = selectedIndexes.filter((indexOption) => + clusterNames.includes(indexOption.cluster) + ); + this.onIndexesChange(matchingIndexes, { name: 'index' }, form); + } + + form.setFieldValue( + field.name, + options.map((option) => option.cluster) + ); + this.setState({ selectedClusters: options }); + }; + + renderClusterIndexesOption = (option) => { + const { label, health } = option; + return {label}; + }; + + renderSelectedClusterIndexesOptions = (options = []) => { + // If the cluster name in the option is undefined, it indicates the index is on the local cluster. + const getLabel = ({ cluster, index }) => + `${index} (${cluster ? cluster : this.state.localClusterName})`; + return options.map((option) => ({ + ...option, + label: getLabel(option), + })); + }; + + onIndexesChange = (options, field, form) => { + const selectedIndexes = this.renderSelectedClusterIndexesOptions(options); + form.setFieldValue(field.name, selectedIndexes); + this.setState({ selectedIndexes: selectedIndexes }); + }; + + onCreateOption = (value, field, form) => { + const { localClusterName, selectedIndexes } = this.state; + let clusterName = localClusterName; + let indexName = value; + if (value.includes(':')) { + const splitValue = value.split(':'); + clusterName = splitValue[0]; + indexName = splitValue[1]; + } + selectedIndexes.push({ + label: indexName, + health: undefined, + index: indexName, + cluster: clusterName, + value: clusterName === localClusterName ? indexName : `${clusterName}:${indexName}`, + }); + form.setFieldValue(field.name, selectedIndexes); + this.setState({ selectedIndexes: selectedIndexes }); + }; + + render() { + const { monitorType } = this.props; + const { + loading, + clusterCount, + clusterOptions, + selectedClusters, + indexOptions, + selectedIndexes, + } = this.state; + + return ( + <> + + + + Select clusters + + + Select a local cluster or remote clusters from cross-cluster connections.{' '} + + Learn more + + +
+ ), + style: { paddingLeft: '10px' }, + }} + inputProps={{ + isLoading: loading, + // Disable cluster selection field when loading, or when there is only 1 cluster. + isDisabled: loading || clusterCount <= 1, + options: clusterOptions, + renderOption: this.renderClusterOption, + onChange: this.onClustersChange, + selectedOptions: selectedClusters, + 'data-test-subj': 'clustersComboBox', + }} + /> + + + + {monitorType !== MONITOR_TYPE.CLUSTER_METRICS && ( + + + Indexes + + + Select one or more indexes or wildcard patterns + + + ), + helpText: + 'You can use * as a wildcard or date math index resolution in your index pattern.', + style: { paddingLeft: '10px' }, + }} + inputProps={{ + isLoading: loading, + isDisabled: loading || selectedClusters.length < 1, + options: indexOptions, + renderOption: this.renderClusterIndexesOption, + onChange: this.onIndexesChange, + onCreateOption: this.onCreateOption, + selectedOptions: selectedIndexes, + 'data-test-subj': 'indicesComboBox', + }} + /> + )} + + ); + } +} + +export default connect(CrossClusterConfiguration); diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/index.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/index.js new file mode 100644 index 000000000..afd93d0da --- /dev/null +++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import CrossClusterConfiguration from './CrossClusterConfiguration'; + +export default CrossClusterConfiguration; diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers.js new file mode 100644 index 000000000..408a0e924 --- /dev/null +++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers.js @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import _ from 'lodash'; +import { DEFAULT_EMPTY_DATA, MONITOR_TYPE } from '../../../../../utils/constants'; + +export const getLocalClusterName = async (httpClient) => { + let localClusterName = DEFAULT_EMPTY_DATA; + try { + const response = await httpClient.get('../api/alerting/_health'); + if (response.ok) { + localClusterName = response.resp[0]?.cluster; + } else { + console.log('Error getting local cluster name:', response); + } + } catch (e) { + console.error(e); + } + return localClusterName; +}; + +export const getDataSources = (monitor, localClusterName) => { + const monitorType = _.get( + monitor, + 'monitor_type', + _.get(monitor, 'ui_metadata.monitor_type', MONITOR_TYPE.QUERY_LEVEL) + ); + let dataSources; + switch (monitorType) { + case MONITOR_TYPE.CLUSTER_METRICS: + dataSources = _.get(monitor, 'inputs.0.uri.clusters'); + // To preserve functionality of legacy monitors, cluster metrics monitors run on the + // local cluster by default if no clusters are specified in the monitor configuration. + if (_.isEmpty(dataSources)) dataSources = [localClusterName || DEFAULT_EMPTY_DATA]; + break; + case MONITOR_TYPE.DOC_LEVEL: + dataSources = _.get(monitor, 'inputs.0.doc_level_input.indices', [DEFAULT_EMPTY_DATA]); + break; + default: + dataSources = _.get(monitor, 'inputs.0.search.indices', [DEFAULT_EMPTY_DATA]); + } + dataSources = _.sortBy(dataSources); + return dataSources; +}; diff --git a/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js b/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js index 278ad9b31..73a788fa0 100644 --- a/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js +++ b/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js @@ -5,57 +5,154 @@ import React, { Fragment } from 'react'; import _ from 'lodash'; -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; - -import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants'; -import { URL } from '../../../../../utils/constants'; +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { DEFAULT_EMPTY_DATA, MONITOR_TYPE } from '../../../../utils/constants'; import ContentPanel from '../../../../components/ContentPanel'; -const QueryPerformance = ({ response, actions }) => ( - - - Check the performance of your query and make sure to follow best practices.{' '} - - Learn more - - - } - actions={actions} - > - - - - - Monitor duration - - {`${_.get(response, 'took', DEFAULT_EMPTY_DATA)} ms`} - - - - - - Request duration - - {_.get(response, 'invalid.path', DEFAULT_EMPTY_DATA)} - - - - - - Hits - - {_.get(response, 'hits.total.value', DEFAULT_EMPTY_DATA)} - - - - - - +export const RECOMMENDED_DURATION = 100; +export const SEARCH_DOCUMENTATION = 'https://opensearch.org/docs/latest/search-plugins/'; + +const getPerformanceCallOut = () => ( + + We recommend reducing your query size and time range or changing data sources to optimize for + monitor performance.{' '} + + Learn more + + ); +export const getPerformanceModal = ({ edit, onClose, onSubmit, values }) => { + const monitorType = _.get( + values, + 'monitor_type', + _.get(values, 'workflow_type', MONITOR_TYPE.QUERY_LEVEL) + ); + + let hasRemoteClusters; + switch (monitorType) { + case MONITOR_TYPE.CLUSTER_METRICS: + hasRemoteClusters = !_.isEmpty(_.get(values, 'uri.clusters', [])); + break; + default: + // Indexes for remote clusters will store the index name in + // the 'value' attribute of the object, not the 'label' attribute. + hasRemoteClusters = _.get(values, 'index', []) + .map(({ label, value }) => value || label) + .some((indexName) => indexName.includes(':')); + } + + return ( + + + +

Monitor is not optimized

+
+
+ + + +

The following use cases may impact this monitor's performance.

+
    + {hasRemoteClusters &&
  • One or more remote indexes may affect monitor accuracy
  • } +
  • Large queries may impact monitor and remote cluster performance
  • +
+
+
+ + + + {edit ? 'Update' : 'Create'} anyway + + + + Reconfigure + + +
+ ); +}; + +const QueryPerformance = ({ response, actions }) => { + const monitorDuration = _.get(response, 'took', DEFAULT_EMPTY_DATA); + const monitorDurationCallout = monitorDuration >= RECOMMENDED_DURATION; + + // TODO: Need to confirm the purpose of requestDuration. + // There's no explanation for it in the frontend code even back to opendistro implementation. + const requestDuration = _.get(response, 'invalid.path', DEFAULT_EMPTY_DATA); + const requestDurationCallout = requestDuration >= RECOMMENDED_DURATION; + const displayPerfCallOut = monitorDurationCallout || requestDurationCallout; + const alertIcon = ( + <> +   + + + ); + return ( + + {displayPerfCallOut && ( + <> + {getPerformanceCallOut()} + + + )} + + + + + + + Monitor duration + + {`${monitorDuration} ms`} + {monitorDurationCallout ? alertIcon : undefined} + + + + + + Request duration + + {requestDuration} + {requestDurationCallout ? alertIcon : undefined} + + + + + + Hits + + {_.get(response, 'hits.total.value', DEFAULT_EMPTY_DATA)} + + + + + + + ); +}; + export default QueryPerformance; diff --git a/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap b/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap index fa380a6b7..68b77018f 100644 --- a/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap +++ b/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap @@ -36,27 +36,7 @@ exports[`QueryPerformance renders 1`] = ` >
- - Check the performance of your query and make sure to follow best practices. - - Learn more -
- EuiIconMock -
- - (opens in a new tab or window) - -
-
-
+ />
= RECOMMENDED_DURATION; + + // TODO: Need to confirm the purpose of requestDuration. + // There's no explanation for it in the frontend code even back to opendistro implementation. + const requestDurationCallout = + _.get(performanceResponse, 'invalid.path') >= RECOMMENDED_DURATION; + const displayPerfCallOut = monitorDurationCallout || requestDurationCallout; + + if (!createModalOpen && displayPerfCallOut) { + this.setState({ + createModalOpen: true, + formikBag: formikBag, + }); + } else { + this.onSubmit(values, formikBag); + } + } + onSubmit(values, formikBag) { const { edit, history, updateMonitor, notifications, httpClient } = this.props; const { triggerToEdit } = this.state; @@ -134,11 +162,15 @@ export default class CreateMonitor extends Component { isDarkMode, notificationService, } = this.props; - const { initialValues, plugins } = this.state; + const { createModalOpen, initialValues, plugins } = this.state; return (
- + {({ values, errors, handleSubmit, isSubmitting, isValid, touched }) => { const isComposite = values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL; @@ -239,6 +271,23 @@ export default class CreateMonitor extends Component { }) } /> + + {createModalOpen && + getPerformanceModal({ + edit: edit, + onClose: () => { + this.state.formikBag.setSubmitting(false); + this.setState({ + createModalOpen: false, + formikBag: undefined, + }); + }, + onSubmit: () => { + this.onSubmit(values, this.state.formikBag); + this.setState({ createModalOpen: false }); + }, + values: values, + })} ); }} diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap index 6400e4709..598937daf 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap @@ -23,6 +23,7 @@ exports[`CreateMonitor renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -64,6 +65,7 @@ exports[`CreateMonitor renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap index 4a180d30c..1456896ef 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap @@ -49,6 +49,7 @@ exports[`formikToClusterMetricsUri can build a ClusterMetricsMonitor request wit Object { "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", @@ -60,6 +61,7 @@ exports[`formikToClusterMetricsUri can build a ClusterMetricsMonitor request wit Object { "uri": Object { "api_type": "CLUSTER_HEALTH", + "clusters": Array [], "path": "_cluster/health", "path_params": "", "url": "http://localhost:9200/_cluster/health", diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index 9e35830be..39c2de490 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -49,8 +49,10 @@ export const FORMIK_INITIAL_VALUES = { /* DEFINE MONITOR */ monitor_type: MONITOR_TYPE.QUERY_LEVEL, searchType: 'graph', + clusterNames: [], uri: { api_type: '', + clusters: [], path: '', path_params: '', url: '', diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js index db7c6aedd..4619c08b2 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js @@ -170,12 +170,15 @@ export function formikToClusterMetricsInput(values) { url = url + pathParams + _.get(API_TYPES, `${apiType}.appendText`, ''); } } + const clusterNames = _.get(values, 'clusterNames', []); + return { uri: { api_type: apiType, path: path, path_params: pathParams, url: url, + clusters: clusterNames, }, }; } @@ -204,7 +207,10 @@ export function formikToUiSearch(values) { } export function formikToIndices(values) { - return values.index.map(({ label }) => label); + const hasRemoteClusters = values.index.some( + ({ cluster, value }) => !_.isEmpty(cluster) && !_.isEmpty(value) + ); + return values.index.map(({ label, value }) => (hasRemoteClusters ? value : label)); } export function formikToQuery(values) { diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js index 999197c78..1375cbebe 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js @@ -37,6 +37,8 @@ export default function monitorToFormik(monitor) { return { index: FORMIK_INITIAL_VALUES.index, uri: inputs[0].uri, + clusterNames: inputs[0].uri.clusters || [], + searchType: SEARCH_TYPE.CLUSTER_METRICS, }; case MONITOR_TYPE.DOC_LEVEL: return docLevelInputToFormik(monitor); diff --git a/public/pages/CreateMonitor/containers/DataSource/DataSource.js b/public/pages/CreateMonitor/containers/DataSource/DataSource.js index 2ca2b61c5..fa9029863 100644 --- a/public/pages/CreateMonitor/containers/DataSource/DataSource.js +++ b/public/pages/CreateMonitor/containers/DataSource/DataSource.js @@ -33,15 +33,26 @@ class DataSource extends Component { } render() { - const { isMinimal } = this.props; + const { isMinimal, remoteMonitoringEnabled } = this.props; const { monitor_type, searchType } = this.props.values; const displayTimeField = - searchType === SEARCH_TYPE.GRAPH && monitor_type !== MONITOR_TYPE.DOC_LEVEL; + searchType === SEARCH_TYPE.GRAPH && + monitor_type !== MONITOR_TYPE.DOC_LEVEL && + monitor_type !== MONITOR_TYPE.CLUSTER_METRICS; const monitorIndexDisplay = ( <> - - - {displayTimeField && } + + + {displayTimeField && ( + <> + + + + )} ); diff --git a/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap b/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap index 3182e6526..16b82d6b0 100644 --- a/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap +++ b/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap @@ -20,9 +20,7 @@ exports[`DataSource renders 1`] = ` httpClient={[MockFunction]} monitorType="query_level_monitor" /> - + `; diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js index 1e59a00bb..64fdc91fb 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js +++ b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js @@ -39,6 +39,7 @@ import ConfigureDocumentLevelQueries from '../../components/DocumentLevelMonitor import FindingsDashboard from '../../../Dashboard/containers/FindingsDashboard'; import { validDocLevelGraphQueries } from '../../components/DocumentLevelMonitorQueries/utils/helpers'; import { validateWhereFilters } from '../../components/MonitorExpressions/expressions/utils/whereHelpers'; +import { REMOTE_MONITORING_ENABLED_SETTING_PATH } from '../../components/CrossClusterConfigurations/components/ExperimentalBanner'; function renderEmptyMessage(message) { return ( @@ -74,6 +75,7 @@ class DefineMonitor extends Component { plugins: [], loadingResponse: false, PanelComponent: props.flyoutMode ? ({ children }) => <>{children} : ContentPanel, + remoteMonitoringEnabled: false, }; this.renderGraph = this.renderGraph.bind(this); @@ -88,6 +90,7 @@ class DefineMonitor extends Component { this.getPlugins = this.getPlugins.bind(this); this.getSupportedApiList = this.getSupportedApiList.bind(this); this.showPluginWarning = this.showPluginWarning.bind(this); + this.getSettings = this.getSettings.bind(this); } componentDidMount() { @@ -96,6 +99,7 @@ class DefineMonitor extends Component { const isGraph = searchType === SEARCH_TYPE.GRAPH; const hasIndices = !!index.length; const hasTimeField = !!timeField; + this.getSettings(); if (isGraph && hasIndices) { this.onQueryMappings(); if (hasTimeField || !this.requiresTimeField()) this.onRunQuery(); @@ -175,6 +179,34 @@ class DefineMonitor extends Component { } } + async getSettings() { + try { + const { httpClient } = this.props; + const response = await httpClient.get('../api/alerting/_settings'); + if (response.ok) { + const { defaults, transient, persistent } = response.resp; + let remoteMonitoringEnabled = _.get( + // If present, take the 'transient' setting. + transient, + REMOTE_MONITORING_ENABLED_SETTING_PATH, + // Else take the 'persistent' setting. + _.get( + persistent, + REMOTE_MONITORING_ENABLED_SETTING_PATH, + // Else take the 'default' setting. + _.get(defaults, REMOTE_MONITORING_ENABLED_SETTING_PATH, false) + ) + ); + // Boolean settings are returned as strings (e.g., `"true"`, and `"false"`). Constructing boolean value from the string. + if (typeof remoteMonitoringEnabled === 'string') + remoteMonitoringEnabled = JSON.parse(remoteMonitoringEnabled); + this.setState({ remoteMonitoringEnabled: remoteMonitoringEnabled }); + } + } catch (e) { + console.log('Error while retrieving settings', e); + } + } + requiresTimeField() { const { values: { monitor_type, searchType }, @@ -406,7 +438,8 @@ class DefineMonitor extends Component { } async onQueryMappings() { - const index = this.props.values.index.map(({ label }) => label); + // Indexes for remote clusters will store the index name in the 'value' attribute of the object, not the 'label' attribute. + const index = this.props.values.index.map(({ label, value }) => value || label); try { const mappings = await this.queryMappings(index); const dataTypes = getPathsPerDataType(mappings); @@ -417,16 +450,34 @@ class DefineMonitor extends Component { } async queryMappings(index) { - if (!index.length) { - return {}; - } + if (!index.length) return {}; try { - const response = await this.props.httpClient.post('../api/alerting/_mappings', { - body: JSON.stringify({ index }), - }); + // If any index contain ":", it indicates at least 1 remote index is configured. + const hasRemoteClusters = index.some((indexName) => indexName.includes(':')); + const response = hasRemoteClusters + ? await this.props.httpClient.get('../api/alerting/remote/indexes', { + query: { + indexes: index.join(','), + include_mappings: true, + }, + }) + : // Otherwise, all configured indexes are on the local cluster. + await this.props.httpClient.post('../api/alerting/_mappings', { + body: JSON.stringify({ index }), + }); if (response.ok) { - return response.resp; + if (hasRemoteClusters) { + const mappings = {}; + Object.entries(response.resp).forEach(([_, clusterInfo]) => { + Object.entries(clusterInfo.indexes).forEach(([indexName, indexInfo]) => { + mappings[indexName] = { mappings: indexInfo.mappings }; + }); + }); + return mappings; + } else { + return response.resp; + } } return {}; } catch (err) { @@ -528,7 +579,9 @@ class DefineMonitor extends Component { requiresPathParams = _.isEmpty(requiresPathParams); if (!requiresPathParams) { const path = _.get(API_TYPES, `${apiKey}.paths.withoutPathParams`); - const values = { uri: { ...FORMIK_INITIAL_VALUES.uri, path } }; + const values = { + uri: { ...FORMIK_INITIAL_VALUES.uri, path, clusterNames: [] }, + }; requests.push(buildClusterMetricsRequest(values)); } }); @@ -593,16 +646,27 @@ class DefineMonitor extends Component { } render() { - const { values, errors, httpClient, detectorId, notifications, isDarkMode, flyoutMode } = - this.props; - const { dataTypes, PanelComponent } = this.state; + const { + values, + values: { monitor_type }, + errors, + httpClient, + detectorId, + notifications, + isDarkMode, + flyoutMode, + } = this.props; + const { dataTypes, PanelComponent, remoteMonitoringEnabled } = this.state; const monitorContent = this.getMonitorContent(); const { searchType } = this.props.values; - const isGraphOrQuery = searchType === SEARCH_TYPE.GRAPH || searchType === SEARCH_TYPE.QUERY; + const displayDataSourcePanel = + searchType === SEARCH_TYPE.GRAPH || + searchType === SEARCH_TYPE.QUERY || + (remoteMonitoringEnabled && monitor_type === MONITOR_TYPE.CLUSTER_METRICS); return (
- {!flyoutMode && isGraphOrQuery && ( + {!flyoutMode && displayDataSourcePanel && (
diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap index d887a6adb..a0c25c035 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap @@ -8,6 +8,7 @@ exports[`DefineMonitor renders 1`] = ` errors={Object {}} httpClient={[MockFunction]} isMinimal={false} + remoteMonitoringEnabled={false} values={ Object { "aggregationType": "count", @@ -21,6 +22,7 @@ exports[`DefineMonitor renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -62,6 +64,7 @@ exports[`DefineMonitor renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js b/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js index dacd8f209..0809cfbda 100644 --- a/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js +++ b/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js @@ -12,6 +12,7 @@ import { FormikComboBox } from '../../../../components/FormControls'; import { validateIndex, hasError, isInvalid } from '../../../../utils/validate'; import { canAppendWildcard, createReasonableWait, getMatchedOptions } from './utils/helpers'; import { MONITOR_TYPE } from '../../../../utils/constants'; +import CrossClusterConfiguration from '../../components/CrossClusterConfigurations/containers'; const CustomOption = ({ option, searchValue, contentClassName }) => { const { health, label, index } = option; @@ -197,6 +198,7 @@ class MonitorIndex extends React.Component { } render() { + const { httpClient, remoteMonitoringEnabled } = this.props; const { isLoading, allIndices, @@ -217,42 +219,62 @@ class MonitorIndex extends React.Component { false //isIncludingSystemIndices ); - const supportMultipleIndices = this.props.monitorType !== MONITOR_TYPE.DOC_LEVEL; + let supportMultipleIndices = true; + let supportsCrossClusterMonitoring = false; + switch (this.props.monitorType) { + case MONITOR_TYPE.DOC_LEVEL: + supportMultipleIndices = false; + supportsCrossClusterMonitoring = false; + break; + case MONITOR_TYPE.BUCKET_LEVEL: + case MONITOR_TYPE.CLUSTER_METRICS: + case MONITOR_TYPE.QUERY_LEVEL: + supportsCrossClusterMonitoring = true; + break; + default: + } return ( - { - form.setFieldTouched('index', true); - }, - onChange: (options, field, form) => { - form.setFieldValue('index', options); - }, - onCreateOption: (value, field, form) => { - this.onCreateOption(value, field.value, form.setFieldValue, supportMultipleIndices); - }, - onSearchChange: this.onSearchChange, - renderOption: this.renderOption, - isClearable: true, - singleSelection: supportMultipleIndices ? false : { asPlainText: true }, - 'data-test-subj': 'indicesComboBox', - }} - /> + <> + {remoteMonitoringEnabled && supportsCrossClusterMonitoring ? ( + + ) : ( + { + form.setFieldTouched('index', true); + }, + onChange: (options, field, form) => { + form.setFieldValue('index', options); + }, + onCreateOption: (value, field, form) => { + this.onCreateOption(value, field.value, form.setFieldValue, supportMultipleIndices); + }, + onSearchChange: this.onSearchChange, + renderOption: this.renderOption, + delimiter: ',', + isClearable: true, + singleSelection: supportMultipleIndices ? false : { asPlainText: true }, + 'data-test-subj': 'indicesComboBox', + }} + /> + )} + ); } } diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap index 2c0d6ed4f..5c818aa44 100644 --- a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap +++ b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap @@ -15,6 +15,7 @@ exports[`MonitorIndex renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -56,6 +57,7 @@ exports[`MonitorIndex renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", @@ -86,6 +88,7 @@ exports[`MonitorIndex renders 1`] = ` Object { "async": true, "data-test-subj": "indicesComboBox", + "delimiter": ",", "isClearable": true, "isLoading": true, "onBlur": [Function], @@ -160,6 +163,7 @@ exports[`MonitorIndex renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -201,6 +205,7 @@ exports[`MonitorIndex renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", @@ -251,6 +256,7 @@ exports[`MonitorIndex renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -292,6 +298,7 @@ exports[`MonitorIndex renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", @@ -406,6 +413,7 @@ exports[`MonitorIndex renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -447,6 +455,7 @@ exports[`MonitorIndex renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", @@ -497,6 +506,7 @@ exports[`MonitorIndex renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -538,6 +548,7 @@ exports[`MonitorIndex renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", @@ -559,6 +570,7 @@ exports[`MonitorIndex renders 1`] = ` Object { "async": true, "data-test-subj": "indicesComboBox", + "delimiter": ",", "isClearable": true, "isLoading": true, "onBlur": [Function], @@ -588,6 +600,7 @@ exports[`MonitorIndex renders 1`] = ` async={true} compressed={false} data-test-subj="indicesComboBox" + delimiter="," fullWidth={false} id="index" isClearable={true} diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap index 0b146e70c..040b38d27 100644 --- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap +++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap @@ -44,6 +44,7 @@ exports[`AcknowledgeAlertsModal renders 1`] = ` "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, + "clusterNames": Array [], "cronExpression": "0 */1 * * *", "daily": 0, "description": "", @@ -85,6 +86,7 @@ exports[`AcknowledgeAlertsModal renders 1`] = ` "timezone": Array [], "uri": Object { "api_type": "", + "clusters": Array [], "path": "", "path_params": "", "url": "", diff --git a/public/pages/Dashboard/containers/Dashboard.js b/public/pages/Dashboard/containers/Dashboard.js index cea536244..f55d344f5 100644 --- a/public/pages/Dashboard/containers/Dashboard.js +++ b/public/pages/Dashboard/containers/Dashboard.js @@ -40,6 +40,7 @@ import { MAX_ALERT_COUNT } from '../utils/constants'; import AcknowledgeAlertsModal from '../components/AcknowledgeAlertsModal'; import { getAlertsFindingColumn } from '../components/FindingsDashboard/findingsUtils'; import { ChainedAlertDetailsFlyout } from '../components/ChainedAlertDetailsFlyout/ChainedAlertDetailsFlyout'; +import { CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN } from '../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants'; export default class Dashboard extends Component { constructor(props) { @@ -364,6 +365,10 @@ export default class Dashboard extends Component { case MONITOR_TYPE.BUCKET_LEVEL: columnType = insertGroupByColumn(groupBy); break; + case MONITOR_TYPE.CLUSTER_METRICS: + columnType = _.cloneDeep(queryColumns); + columnType.push(CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN); + break; case MONITOR_TYPE.DOC_LEVEL: columnType = _.cloneDeep(queryColumns); columnType.splice( diff --git a/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js b/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js index 038073d9f..feec3ccd3 100644 --- a/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js +++ b/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js @@ -20,16 +20,10 @@ const MonitorOverview = ({ detector, detectorId, delegateMonitors, + localClusterName, + setFlyout, }) => { const [flyoutData, setFlyoutData] = useState(undefined); - const items = getOverviewStats( - monitor, - monitorId, - monitorVersion, - activeCount, - detector, - detectorId - ); let relatedMonitorsStat = null; let relatedMonitorsData = null; @@ -76,6 +70,16 @@ const MonitorOverview = ({ const onFlyoutClose = () => setFlyoutData(undefined); + const items = getOverviewStats( + monitor, + monitorId, + monitorVersion, + activeCount, + detector, + detectorId, + localClusterName, + setFlyout + ); return ( <> {flyoutData && ( diff --git a/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap b/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap index 69cc25cf8..c68b27eb4 100644 --- a/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap +++ b/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap @@ -75,6 +75,20 @@ exports[`MonitorOverview renders 1`] = `
+
+
+ + Data sources + +
+ - +
+
+
diff --git a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js index 30fa20829..d2c134aa9 100644 --- a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js +++ b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js @@ -5,7 +5,7 @@ import React from 'react'; import _ from 'lodash'; -import { EuiIcon, EuiLink } from '@elastic/eui'; +import { EuiBadge, EuiLink } from '@elastic/eui'; import moment from 'moment-timezone'; import getScheduleFromMonitor from './getScheduleFromMonitor'; import { @@ -16,6 +16,8 @@ import { } from '../../../../../utils/constants'; import { API_TYPES } from '../../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants'; import { getApiType } from '../../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; +import { DATA_SOURCES_FLYOUT_TYPE } from '../../../../../components/Flyout/flyouts/dataSources'; +import { getDataSources } from '../../../../CreateMonitor/components/CrossClusterConfigurations/utils/helpers'; // TODO: used in multiple places, move into helper export function getTime(time) { @@ -59,13 +61,56 @@ function getMonitorLevelType(monitorType) { } } +const getDataSourcesDisplay = ( + dataSources = [], + localClusterName = DEFAULT_EMPTY_DATA, + monitorType, + setFlyout +) => { + const closeFlyout = () => { + if (typeof setFlyout === 'function') setFlyout(null); + }; + + const openFlyout = () => { + if (typeof setFlyout === 'function') { + setFlyout({ + type: DATA_SOURCES_FLYOUT_TYPE, + payload: { + closeFlyout: closeFlyout, + dataSources: dataSources, + localClusterName: localClusterName, + monitorType: monitorType, + }, + }); + } + }; + + return dataSources.length <= 1 ? ( + dataSources[0] || localClusterName + ) : ( + <> + {dataSources[0]}  + + View all {dataSources.length} + + + ); +}; + export default function getOverviewStats( monitor, monitorId, monitorVersion, activeCount, detector, - detectorId + detectorId, + localClusterName, + setFlyout ) { const searchType = _.has(monitor, 'inputs[0].uri') ? SEARCH_TYPE.CLUSTER_METRICS @@ -90,6 +135,9 @@ export default function getOverviewStats( if (!monitorLevelType) { monitorLevelType = _.get(monitor, 'ui_metadata.monitor_type', 'query_level_monitor'); } + + const dataSources = getDataSources(monitor, localClusterName); + const overviewStats = [ { header: 'Monitor type', @@ -100,6 +148,10 @@ export default function getOverviewStats( value: getMonitorType(searchType, monitor), }, ...detectorOverview, + { + header: 'Data sources', + value: getDataSourcesDisplay(dataSources, localClusterName, monitorLevelType, setFlyout), + }, { header: 'Total active alerts', value: activeCount, diff --git a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js index d9c567dfe..3f385e1fc 100644 --- a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js +++ b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js @@ -26,6 +26,10 @@ describe('getOverviewStats', () => { header: 'Monitor definition type', value: 'Extraction Query', }, + { + header: 'Data sources', + value: DEFAULT_EMPTY_DATA, + }, { header: 'Total active alerts', value: activeCount, diff --git a/public/pages/MonitorDetails/containers/MonitorDetails.js b/public/pages/MonitorDetails/containers/MonitorDetails.js index d33f153ef..d3a673dc3 100644 --- a/public/pages/MonitorDetails/containers/MonitorDetails.js +++ b/public/pages/MonitorDetails/containers/MonitorDetails.js @@ -47,6 +47,7 @@ import monitorToFormik from '../../CreateMonitor/containers/CreateMonitor/utils/ import FindingsDashboard from '../../Dashboard/containers/FindingsDashboard'; import { TABLE_TAB_IDS } from '../../Dashboard/components/FindingsDashboard/findingsUtils'; import { DeleteMonitorModal } from '../../../components/DeleteModal/DeleteMonitorModal'; +import { getLocalClusterName } from '../../CreateMonitor/components/CrossClusterConfigurations/utils/helpers'; export default class MonitorDetails extends Component { constructor(props) { @@ -72,6 +73,7 @@ export default class MonitorDetails extends Component { isJsonModalOpen: false, tabId: TABLE_TAB_IDS.ALERTS.id, showDeleteModal: false, + localClusterName: undefined, }; } @@ -89,6 +91,7 @@ export default class MonitorDetails extends Component { componentDidMount() { this.getMonitor(this.props.match.params.monitorId); + this.getLocalClusterName(); } componentDidUpdate(prevProps, prevState) { @@ -104,6 +107,12 @@ export default class MonitorDetails extends Component { this.props.setFlyout(null); } + getLocalClusterName = async () => { + this.setState({ + localClusterName: await getLocalClusterName(this.props.httpClient), + }); + }; + getDetector = (id) => { const { httpClient, notifications } = this.props; httpClient @@ -407,6 +416,7 @@ export default class MonitorDetails extends Component { isJsonModalOpen, showDeleteModal, delegateMonitors, + localClusterName, } = this.state; const { location, @@ -501,6 +511,8 @@ export default class MonitorDetails extends Component { detector={detector} detectorId={detectorId} delegateMonitors={delegateMonitors} + localClusterName={localClusterName} + setFlyout={setFlyout} /> { - switch (monitor.monitor_type) { - case MONITOR_TYPE.BUCKET_LEVEL: - return trigger[TRIGGER_TYPE.BUCKET_LEVEL]; - case MONITOR_TYPE.DOC_LEVEL: - return trigger[TRIGGER_TYPE.DOC_LEVEL]; - case MONITOR_TYPE.COMPOSITE_LEVEL: - return trigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; - default: - return trigger[TRIGGER_TYPE.QUERY_LEVEL]; + let unwrappedTrigger = trigger; + if (Object.keys(trigger).length === 1) { + switch (monitor.monitor_type) { + case MONITOR_TYPE.BUCKET_LEVEL: + unwrappedTrigger = trigger[TRIGGER_TYPE.BUCKET_LEVEL]; + break; + case MONITOR_TYPE.DOC_LEVEL: + unwrappedTrigger = trigger[TRIGGER_TYPE.DOC_LEVEL]; + break; + case MONITOR_TYPE.COMPOSITE_LEVEL: + unwrappedTrigger = trigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; + break; + case MONITOR_TYPE.CLUSTER_METRICS: + case MONITOR_TYPE.QUERY_LEVEL: + default: + unwrappedTrigger = trigger[TRIGGER_TYPE.QUERY_LEVEL]; + break; + } } + return unwrappedTrigger; }); } diff --git a/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap b/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap index cb7cc4c55..a73f0d641 100644 --- a/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap +++ b/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap @@ -40,7 +40,15 @@ exports[`Triggers renders 1`] = ` itemId="id" items={ Array [ - undefined, + Object { + "actions": Array [ + Object { + "name": "Random Action", + }, + ], + "name": "Random Trigger", + "severity": 1, + }, ] } key="0" diff --git a/server/clusters/alerting/alertingPlugin.js b/server/clusters/alerting/alertingPlugin.js index 65d26315f..a8c7f026e 100644 --- a/server/clusters/alerting/alertingPlugin.js +++ b/server/clusters/alerting/alertingPlugin.js @@ -10,6 +10,7 @@ import { EMAIL_ACCOUNT_BASE_API, EMAIL_GROUP_BASE_API, WORKFLOW_BASE_API, + CROSS_CLUSTER_BASE_API, } from '../../services/utils/constants'; export default function alertingPlugin(Client, config, components) { @@ -420,4 +421,22 @@ export default function alertingPlugin(Client, config, components) { }, method: 'GET', }); + + alerting.getRemoteIndexes = ca({ + url: { + fmt: `${CROSS_CLUSTER_BASE_API}/indexes?indexes=<%=indexes%>&include_mappings=<%=include_mappings%>`, + req: { + indexes: { + type: 'string', + required: true, + }, + include_mappings: { + type: 'boolean', + required: false, + }, + }, + }, + needBody: true, + method: 'GET', + }); } diff --git a/server/plugin.js b/server/plugin.js index b21b66ac5..901c6acd8 100644 --- a/server/plugin.js +++ b/server/plugin.js @@ -12,8 +12,17 @@ import { MonitorService, AnomalyDetectorService, FindingService, + CrossClusterService, } from './services'; -import { alerts, destinations, opensearch, monitors, detectors, findings } from '../server/routes'; +import { + alerts, + destinations, + opensearch, + monitors, + detectors, + findings, + crossCluster, +} from '../server/routes'; export class AlertingPlugin { constructor(initializerContext) { @@ -36,6 +45,7 @@ export class AlertingPlugin { const destinationsService = new DestinationsService(alertingESClient); const anomalyDetectorService = new AnomalyDetectorService(adESClient); const findingService = new FindingService(alertingESClient); + const crossClusterService = new CrossClusterService(alertingESClient); const services = { alertService, destinationsService, @@ -43,6 +53,7 @@ export class AlertingPlugin { monitorService, anomalyDetectorService, findingService, + crossClusterService, }; // Create router @@ -54,6 +65,7 @@ export class AlertingPlugin { monitors(services, router); detectors(services, router); findings(services, router); + crossCluster(services, router); return {}; } diff --git a/server/routes/crossCluster.js b/server/routes/crossCluster.js new file mode 100644 index 000000000..883a1a723 --- /dev/null +++ b/server/routes/crossCluster.js @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; + +export default function (services, router) { + const { crossClusterService } = services; + + router.get( + { + path: '/api/alerting/remote/indexes', + validate: { + query: schema.object({ + indexes: schema.string(), + include_mappings: schema.maybe(schema.boolean()), + }), + }, + }, + crossClusterService.getRemoteIndexes + ); +} diff --git a/server/routes/index.js b/server/routes/index.js index 029623900..095c03fc7 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -9,5 +9,6 @@ import opensearch from './opensearch'; import monitors from './monitors'; import detectors from './anomalyDetector'; import findings from './findings'; +import crossCluster from './crossCluster'; -export { alerts, destinations, opensearch, monitors, detectors, findings }; +export { alerts, destinations, opensearch, monitors, detectors, findings, crossCluster }; diff --git a/server/routes/opensearch.js b/server/routes/opensearch.js index 1bb0ab70d..28ade243b 100644 --- a/server/routes/opensearch.js +++ b/server/routes/opensearch.js @@ -69,4 +69,12 @@ export default function (services, router) { }, opensearchService.getSettings ); + + router.get( + { + path: '/api/alerting/_health', + validate: false, + }, + opensearchService.getClusterHealth + ); } diff --git a/server/services/CrossClusterService.js b/server/services/CrossClusterService.js new file mode 100644 index 000000000..78c284bff --- /dev/null +++ b/server/services/CrossClusterService.js @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export default class CrossClusterService { + constructor(esDriver) { + this.esDriver = esDriver; + } + + getRemoteIndexes = async (context, req, res) => { + try { + const { callAsCurrentUser } = await this.esDriver.asScoped(req); + const response = await callAsCurrentUser('alerting.getRemoteIndexes', req.query); + + return res.ok({ + body: { + ok: true, + resp: response, + }, + }); + } catch (err) { + console.error('Alerting - CrossClusterService - getRemoteIndexes:', err); + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + }; +} diff --git a/server/services/OpensearchService.js b/server/services/OpensearchService.js index 30b28c499..feafba9cf 100644 --- a/server/services/OpensearchService.js +++ b/server/services/OpensearchService.js @@ -95,6 +95,30 @@ export default class OpensearchService { } }; + getClusterHealth = async (context, req, res) => { + try { + const { callAsCurrentUser } = this.esDriver.asScoped(req); + const health = await callAsCurrentUser('cat.health', { + format: 'json', + h: 'cluster,status', + }); + return res.ok({ + body: { + ok: true, + resp: health, + }, + }); + } catch (err) { + console.error('Alerting - OpensearchService - getClusterHealth:', err); + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + }; + getMappings = async (context, req, res) => { try { const { index } = req.body; diff --git a/server/services/index.js b/server/services/index.js index 6e3da1d6e..277b9c678 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -9,6 +9,7 @@ import OpensearchService from './OpensearchService'; import MonitorService from './MonitorService'; import AnomalyDetectorService from './AnomalyDetectorService'; import FindingService from './FindingService'; +import CrossClusterService from './CrossClusterService'; export { AlertService, @@ -17,4 +18,5 @@ export { MonitorService, AnomalyDetectorService, FindingService, + CrossClusterService, }; diff --git a/server/services/utils/constants.js b/server/services/utils/constants.js index c8ba9897a..12c5a45e4 100644 --- a/server/services/utils/constants.js +++ b/server/services/utils/constants.js @@ -6,6 +6,7 @@ export const API_ROUTE_PREFIX = '/_plugins/_alerting'; export const MONITOR_BASE_API = `${API_ROUTE_PREFIX}/monitors`; export const WORKFLOW_BASE_API = `${API_ROUTE_PREFIX}/workflows`; +export const CROSS_CLUSTER_BASE_API = `${API_ROUTE_PREFIX}/remote`; export const AD_BASE_API = `/_plugins/_anomaly_detection/detectors`; export const DESTINATION_BASE_API = `${API_ROUTE_PREFIX}/destinations`; export const EMAIL_ACCOUNT_BASE_API = `${DESTINATION_BASE_API}/email_accounts`;