mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Monitoring] Support for unlinked deployments (#28278)
* Unlinked deployment working for beats * Use better constant * Show N/A for license * Rename to Unlinked Cluster * Use callout to mention unlinked cluster * PR feedback * Use fragment * Speed up the query by using terminate_after * Handle failures more defensively * Remove unnecessary msearch * PR feedback * PR feedback and a bit of light refactor * Updated text * Add api integration tests * Localize call out * Update loc pattern * Fix improper i18n.translate usage * Revert "Fix improper i18n.translate usage" This reverts commit0e2e7608c3
. * Revert "Update loc pattern" This reverts commitcc99fe8a8a
. * Ensure the unlinked deployment cluster counts as a valid cluster * Sometimes, you miss the smallest things * Ensure the unlinked cluster is supported, in that users can click the link and load it * Update tests * PR feedback. Simplifying the flag supported code and adding more tests * Update naming * Rename to Standalone Cluster * Remove unnecessary file * Move logic for setting isSupported to exclusively in flag supported clusters code, update tests
This commit is contained in:
parent
50ec75f800
commit
9f37c1fd0a
26 changed files with 2643 additions and 115 deletions
|
@ -150,3 +150,5 @@ export const DEBOUNCE_FAST_MS = 10; // roughly how long it takes to render a fra
|
|||
* Configuration key for setting the email address used for cluster alert notifications.
|
||||
*/
|
||||
export const CLUSTER_ALERTS_ADDRESS_CONFIG_KEY = 'cluster_alerts.email_notifications.email_address';
|
||||
|
||||
export const STANDALONE_CLUSTER_CLUSTER_UUID = '__standalone_cluster__';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { Listing } from './listing';
|
|
@ -3,20 +3,20 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { capitalize, partial } from 'lodash';
|
||||
import React, { Fragment, Component } from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import moment from 'moment';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import chrome from 'ui/chrome';
|
||||
import { capitalize, partial } from 'lodash';
|
||||
import {
|
||||
EuiHealth,
|
||||
EuiLink,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
EuiIcon
|
||||
} from '@elastic/eui';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { EuiMonitoringTable } from 'plugins/monitoring/components/table';
|
||||
|
@ -24,11 +24,14 @@ import { Tooltip } from 'plugins/monitoring/components/tooltip';
|
|||
import { AlertsIndicator } from 'plugins/monitoring/components/cluster/listing/alerts_indicator';
|
||||
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants';
|
||||
|
||||
const IsClusterSupported = ({ isSupported, children }) => {
|
||||
return isSupported ? children : '-';
|
||||
};
|
||||
|
||||
const STANDALONE_CLUSTER_STORAGE_KEY = 'viewedStandaloneCluster';
|
||||
|
||||
/*
|
||||
* This checks if alerts feature is supported via monitoring cluster
|
||||
* license. If the alerts feature is not supported because the prod cluster
|
||||
|
@ -195,6 +198,17 @@ const getColumns = (
|
|||
sortable: true,
|
||||
render: (licenseType, cluster) => {
|
||||
const license = cluster.license;
|
||||
|
||||
if (!licenseType) {
|
||||
return (
|
||||
<div>
|
||||
<div className="monTableCell__clusterCellLiscense">
|
||||
N/A
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (license) {
|
||||
const licenseExpiry = () => {
|
||||
if (license.expiry_date_in_millis < moment().valueOf()) {
|
||||
|
@ -342,72 +356,112 @@ const handleClickInvalidLicense = (scope, clusterName) => {
|
|||
});
|
||||
};
|
||||
|
||||
const uiModule = uiModules.get('monitoring/directives', []);
|
||||
uiModule.directive('monitoringClusterListing', ($injector) => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
clusters: '=',
|
||||
sorting: '=',
|
||||
filterText: '=',
|
||||
paginationSettings: '=pagination',
|
||||
onTableChange: '=',
|
||||
},
|
||||
link(scope, $el) {
|
||||
const globalState = $injector.get('globalState');
|
||||
const kbnUrl = $injector.get('kbnUrl');
|
||||
const showLicenseExpiration = $injector.get('showLicenseExpiration');
|
||||
|
||||
const _changeCluster = partial(changeCluster, scope, globalState, kbnUrl);
|
||||
const _handleClickIncompatibleLicense = partial(handleClickIncompatibleLicense, scope);
|
||||
const _handleClickInvalidLicense = partial(handleClickInvalidLicense, scope);
|
||||
|
||||
const { sorting, pagination, onTableChange } = scope;
|
||||
|
||||
scope.$watch('clusters', (clusters = []) => {
|
||||
const clusterTable = (
|
||||
<I18nProvider>
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiMonitoringTable
|
||||
className="clusterTable"
|
||||
rows={clusters}
|
||||
columns={getColumns(
|
||||
showLicenseExpiration,
|
||||
_changeCluster,
|
||||
_handleClickIncompatibleLicense,
|
||||
_handleClickInvalidLicense
|
||||
)}
|
||||
rowProps={item => {
|
||||
return {
|
||||
'data-test-subj': `clusterRow_${item.cluster_uuid}`
|
||||
};
|
||||
}}
|
||||
sorting={{
|
||||
...sorting,
|
||||
sort: {
|
||||
...sorting.sort,
|
||||
field: 'cluster_name'
|
||||
}
|
||||
}}
|
||||
pagination={pagination}
|
||||
search={{
|
||||
box: {
|
||||
incremental: true,
|
||||
placeholder: scope.filterText
|
||||
},
|
||||
}}
|
||||
onTableChange={onTableChange}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</I18nProvider>
|
||||
);
|
||||
render(clusterTable, $el[0]);
|
||||
});
|
||||
export class Listing extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
[STANDALONE_CLUSTER_STORAGE_KEY]: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderStandaloneClusterCallout(changeCluster, storage) {
|
||||
if (storage.get(STANDALONE_CLUSTER_STORAGE_KEY)) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={i18n.translate('xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle', {
|
||||
defaultMessage: 'It looks like you have instances that aren\'t connected to an Elasticsearch cluster.'
|
||||
})}
|
||||
iconType="link"
|
||||
>
|
||||
<p>
|
||||
<EuiLink
|
||||
onClick={() => changeCluster(STANDALONE_CLUSTER_CLUSTER_UUID)}
|
||||
data-test-subj="standaloneClusterLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.monitoring.cluster.listing.standaloneClusterCallOutLink"
|
||||
defaultMessage="View these instances."
|
||||
/>
|
||||
</EuiLink>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.monitoring.cluster.listing.standaloneClusterCallOutText"
|
||||
defaultMessage="Or, click Standalone Cluster in the table below"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink onClick={() => {
|
||||
storage.set(STANDALONE_CLUSTER_STORAGE_KEY, true);
|
||||
this.setState({ [STANDALONE_CLUSTER_STORAGE_KEY]: true });
|
||||
}}
|
||||
>
|
||||
<EuiIcon type="cross"/>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.monitoring.cluster.listing.standaloneClusterCallOutDismiss"
|
||||
defaultMessage="Dismiss"
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { angular, clusters, sorting, pagination, onTableChange } = this.props;
|
||||
|
||||
const _changeCluster = partial(changeCluster, angular.scope, angular.globalState, angular.kbnUrl);
|
||||
const _handleClickIncompatibleLicense = partial(handleClickIncompatibleLicense, angular.scope);
|
||||
const _handleClickInvalidLicense = partial(handleClickInvalidLicense, angular.scope);
|
||||
const hasStandaloneCluster = !!clusters.find(cluster => cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID);
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
{hasStandaloneCluster ? this.renderStandaloneClusterCallout(_changeCluster, angular.storage) : null}
|
||||
<EuiMonitoringTable
|
||||
className="clusterTable"
|
||||
rows={clusters}
|
||||
columns={getColumns(
|
||||
angular.showLicenseExpiration,
|
||||
_changeCluster,
|
||||
_handleClickIncompatibleLicense,
|
||||
_handleClickInvalidLicense
|
||||
)}
|
||||
rowProps={item => {
|
||||
return {
|
||||
'data-test-subj': `clusterRow_${item.cluster_uuid}`
|
||||
};
|
||||
}}
|
||||
sorting={{
|
||||
...sorting,
|
||||
sort: {
|
||||
...sorting.sort,
|
||||
field: 'cluster_name'
|
||||
}
|
||||
}}
|
||||
pagination={pagination}
|
||||
search={{
|
||||
box: {
|
||||
incremental: true,
|
||||
placeholder: angular.scope.filterText
|
||||
},
|
||||
}}
|
||||
onTableChange={onTableChange}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { ElasticsearchPanel } from './elasticsearch_panel';
|
||||
import { KibanaPanel } from './kibana_panel';
|
||||
import { LogstashPanel } from './logstash_panel';
|
||||
|
@ -13,23 +13,32 @@ import { BeatsPanel } from './beats_panel';
|
|||
|
||||
import { EuiPage, EuiPageBody } from '@elastic/eui';
|
||||
import { ApmPanel } from './apm_panel';
|
||||
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants';
|
||||
|
||||
export function Overview(props) {
|
||||
const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID;
|
||||
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<AlertsPanel alerts={props.cluster.alerts} changeUrl={props.changeUrl} />
|
||||
|
||||
<ElasticsearchPanel
|
||||
{...props.cluster.elasticsearch}
|
||||
version={props.cluster.version}
|
||||
ml={props.cluster.ml}
|
||||
changeUrl={props.changeUrl}
|
||||
license={props.cluster.license}
|
||||
showLicenseExpiration={props.showLicenseExpiration}
|
||||
/>
|
||||
|
||||
<KibanaPanel {...props.cluster.kibana} changeUrl={props.changeUrl} />
|
||||
{ !isFromStandaloneCluster ?
|
||||
(
|
||||
<Fragment>
|
||||
<ElasticsearchPanel
|
||||
{...props.cluster.elasticsearch}
|
||||
version={props.cluster.version}
|
||||
ml={props.cluster.ml}
|
||||
changeUrl={props.changeUrl}
|
||||
license={props.cluster.license}
|
||||
showLicenseExpiration={props.showLicenseExpiration}
|
||||
/>
|
||||
<KibanaPanel {...props.cluster.kibana} changeUrl={props.changeUrl} />
|
||||
</Fragment>
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
<LogstashPanel {...props.cluster.logstash} changeUrl={props.changeUrl} />
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import './main';
|
||||
import './chart';
|
||||
import './sparkline';
|
||||
import './cluster/listing';
|
||||
import './elasticsearch/cluster_status';
|
||||
import './elasticsearch/index_summary';
|
||||
import './elasticsearch/node_summary';
|
||||
|
|
|
@ -7,6 +7,18 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants';
|
||||
|
||||
function formatClusters(clusters) {
|
||||
return clusters.map(formatCluster);
|
||||
}
|
||||
|
||||
function formatCluster(cluster) {
|
||||
if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) {
|
||||
cluster.cluster_name = 'Standalone Cluster';
|
||||
}
|
||||
return cluster;
|
||||
}
|
||||
|
||||
const uiModule = uiModules.get('monitoring/clusters');
|
||||
uiModule.service('monitoringClusters', ($injector) => {
|
||||
|
@ -30,9 +42,9 @@ uiModule.service('monitoringClusters', ($injector) => {
|
|||
.then(response => response.data)
|
||||
.then(data => {
|
||||
if (clusterUuid) {
|
||||
return data[0]; // return single cluster
|
||||
return formatCluster(data[0]); // return single cluster
|
||||
}
|
||||
return data; // return set of clusters
|
||||
return formatClusters(data); // return set of clusters
|
||||
})
|
||||
.catch(err => {
|
||||
const Private = $injector.get('Private');
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
<monitoring-main name="listing">
|
||||
<monitoring-cluster-listing
|
||||
pagination-settings="clusters.pagination"
|
||||
sorting="clusters.sorting"
|
||||
on-table-change="clusters.onTableChange"
|
||||
clusters="clusters.data"
|
||||
></monitoring-cluster-listing>
|
||||
<div id="monitoringClusterListingApp"></div>
|
||||
</monitoring-main>
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import uiRoutes from 'ui/routes';
|
||||
import { routeInitProvider } from 'plugins/monitoring/lib/route_init';
|
||||
import { MonitoringViewBaseEuiTableController } from '../../';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import template from './index.html';
|
||||
import { Listing } from '../../../components/cluster/listing';
|
||||
|
||||
const getPageData = $injector => {
|
||||
const monitoringClusters = $injector.get('monitoringClusters');
|
||||
|
@ -42,11 +45,37 @@ uiRoutes.when('/home', {
|
|||
storageKey: 'clusters',
|
||||
getPageData,
|
||||
$scope,
|
||||
$injector
|
||||
$injector,
|
||||
reactNodeId: 'monitoringClusterListingApp'
|
||||
});
|
||||
|
||||
const $route = $injector.get('$route');
|
||||
const kbnUrl = $injector.get('kbnUrl');
|
||||
const globalState = $injector.get('globalState');
|
||||
const storage = $injector.get('localStorage');
|
||||
const showLicenseExpiration = $injector.get('showLicenseExpiration');
|
||||
this.data = $route.current.locals.clusters;
|
||||
|
||||
|
||||
$scope.$watch(() => this.data, data => {
|
||||
this.renderReact(
|
||||
<I18nProvider>
|
||||
<Listing
|
||||
clusters={data}
|
||||
angular={{
|
||||
scope: $scope,
|
||||
globalState,
|
||||
kbnUrl,
|
||||
storage,
|
||||
showLicenseExpiration
|
||||
}}
|
||||
sorting={this.sorting}
|
||||
pagination={this.pagination}
|
||||
onTableChange={this.onTableChange}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -35,6 +35,7 @@ const mockReq = (log, queryResult = {}) => {
|
|||
};
|
||||
const goldLicense = () => ({ license: { type: 'gold' } });
|
||||
const basicLicense = () => ({ license: { type: 'basic' } });
|
||||
const standaloneCluster = () => ({ cluster_uuid: '__standalone_cluster__' });
|
||||
|
||||
describe('Flag Supported Clusters', () => {
|
||||
describe('With multiple clusters in the monitoring data', () => {
|
||||
|
@ -141,6 +142,118 @@ describe('Flag Supported Clusters', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('involving an standalone cluster', () => {
|
||||
it('should ignore the standalone cluster in calculating supported basic clusters', () => {
|
||||
const logStub = sinon.stub();
|
||||
const req = mockReq(logStub, {
|
||||
hits: {
|
||||
hits: [ { _source: { cluster_uuid: 'supported_cluster_uuid' } } ]
|
||||
}
|
||||
});
|
||||
const kbnIndices = [];
|
||||
const clusters = [
|
||||
{ cluster_uuid: 'supported_cluster_uuid', ...basicLicense() },
|
||||
{ cluster_uuid: 'unsupported_cluster_uuid', ...basicLicense() },
|
||||
{ ...standaloneCluster() }
|
||||
];
|
||||
|
||||
return flagSupportedClusters(req, kbnIndices)(clusters)
|
||||
.then(resultClusters => {
|
||||
expect(resultClusters).to.eql([
|
||||
{
|
||||
cluster_uuid: 'supported_cluster_uuid',
|
||||
isSupported: true,
|
||||
...basicLicense()
|
||||
},
|
||||
{
|
||||
cluster_uuid: 'unsupported_cluster_uuid',
|
||||
...basicLicense()
|
||||
},
|
||||
{
|
||||
...standaloneCluster(),
|
||||
isSupported: true,
|
||||
}
|
||||
]);
|
||||
sinon.assert.calledWith(
|
||||
logStub,
|
||||
['debug', 'monitoring-ui', 'supported-clusters'],
|
||||
'Found basic license admin cluster UUID for Monitoring UI support: supported_cluster_uuid.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore the standalone cluster in calculating supported mixed license clusters', () => {
|
||||
const logStub = sinon.stub();
|
||||
const req = mockReq(logStub);
|
||||
const kbnIndices = [];
|
||||
const clusters = [
|
||||
{ cluster_uuid: 'supported_cluster_uuid', ...goldLicense() },
|
||||
{ cluster_uuid: 'unsupported_cluster_uuid', ...basicLicense() },
|
||||
{ ...standaloneCluster() }
|
||||
];
|
||||
|
||||
return flagSupportedClusters(req, kbnIndices)(clusters)
|
||||
.then(resultClusters => {
|
||||
expect(resultClusters).to.eql([
|
||||
{
|
||||
cluster_uuid: 'supported_cluster_uuid',
|
||||
isSupported: true,
|
||||
...goldLicense()
|
||||
},
|
||||
{
|
||||
cluster_uuid: 'unsupported_cluster_uuid',
|
||||
...basicLicense()
|
||||
},
|
||||
{
|
||||
...standaloneCluster(),
|
||||
isSupported: true,
|
||||
}
|
||||
]);
|
||||
sinon.assert.calledWith(
|
||||
logStub,
|
||||
['debug', 'monitoring-ui', 'supported-clusters'],
|
||||
'Found some basic license clusters in monitoring data. Only non-basic will be supported.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore the standalone cluster in calculating supported non-basic clusters', () => {
|
||||
const logStub = sinon.stub();
|
||||
const req = mockReq(logStub);
|
||||
const kbnIndices = [];
|
||||
const clusters = [
|
||||
{ cluster_uuid: 'supported_cluster_uuid_1', ...goldLicense() },
|
||||
{ cluster_uuid: 'supported_cluster_uuid_2', ...goldLicense() },
|
||||
{ ...standaloneCluster() }
|
||||
];
|
||||
|
||||
return flagSupportedClusters(req, kbnIndices)(clusters)
|
||||
.then(resultClusters => {
|
||||
expect(resultClusters).to.eql([
|
||||
{
|
||||
cluster_uuid: 'supported_cluster_uuid_1',
|
||||
isSupported: true,
|
||||
...goldLicense()
|
||||
},
|
||||
{
|
||||
cluster_uuid: 'supported_cluster_uuid_2',
|
||||
isSupported: true,
|
||||
...goldLicense()
|
||||
},
|
||||
{
|
||||
...standaloneCluster(),
|
||||
isSupported: true,
|
||||
}
|
||||
]);
|
||||
sinon.assert.calledWith(
|
||||
logStub,
|
||||
['debug', 'monitoring-ui', 'supported-clusters'],
|
||||
'Found all non-basic cluster licenses. All clusters will be supported.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With single cluster in the monitoring data', () => {
|
||||
|
@ -198,5 +311,24 @@ describe('Flag Supported Clusters', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('involving an standalone cluster', () => {
|
||||
it('should ensure it is supported', () => {
|
||||
const req = mockReq(logStub);
|
||||
const kbnIndices = [];
|
||||
const clusters = [{ ...standaloneCluster() }];
|
||||
return flagSupportedClusters(req, kbnIndices)(clusters)
|
||||
.then(result => {
|
||||
expect(result).to.eql([
|
||||
{ ...standaloneCluster(), isSupported: true, }
|
||||
]);
|
||||
sinon.assert.calledWith(
|
||||
logStub,
|
||||
['debug', 'monitoring-ui', 'supported-clusters'],
|
||||
'Found single cluster in monitoring data.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { get, set, find } from 'lodash';
|
||||
import { checkParam } from '../error_missing_required';
|
||||
import { createTypeFilter } from '../create_query';
|
||||
import { LOGGING_TAG } from '../../../common/constants';
|
||||
import { LOGGING_TAG, STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants';
|
||||
|
||||
async function findSupportedBasicLicenseCluster(req, clusters, kbnIndexPattern, kibanaUuid, serverLog) {
|
||||
checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/findSupportedBasicLicenseCluster');
|
||||
|
@ -51,11 +51,12 @@ async function findSupportedBasicLicenseCluster(req, clusters, kbnIndexPattern,
|
|||
* Flag clusters as supported, which means their monitoring data can be seen in the UI.
|
||||
*
|
||||
* Flagging a Basic licensed cluster as supported when it is part of a multi-cluster environment:
|
||||
* 1. Detect if there are multiple clusters
|
||||
* 2. Detect if all of the different cluster licenses are basic
|
||||
* 3. Make a query to the monitored kibana data to find the "supported" cluster
|
||||
* UUID, which is the cluster associated with *this* Kibana instance.
|
||||
* 4. Flag the cluster object with an `isSupported` boolean
|
||||
* 1. Detect if there any standalone clusters and ignore those for these calculations as they are auto supported
|
||||
* 2. Detect if there are multiple linked clusters
|
||||
* 3. Detect if all of the different linked cluster licenses are basic
|
||||
* 4. Make a query to the monitored kibana data to find the "supported" linked cluster
|
||||
* UUID, which is the linked cluster associated with *this* Kibana instance.
|
||||
* 5. Flag the linked cluster object with an `isSupported` boolean
|
||||
*
|
||||
* Non-Basic license clusters and any cluster in a single-cluster environment
|
||||
* are also flagged as supported in this method.
|
||||
|
@ -75,8 +76,17 @@ export function flagSupportedClusters(req, kbnIndexPattern) {
|
|||
};
|
||||
|
||||
return async function (clusters) {
|
||||
// if multi cluster
|
||||
if (clusters.length > 1) {
|
||||
// Standalone clusters are automatically supported in the UI so ignore those for
|
||||
// our calculations here
|
||||
let linkedClusterCount = 0;
|
||||
for (const cluster of clusters) {
|
||||
if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) {
|
||||
cluster.isSupported = true;
|
||||
} else {
|
||||
linkedClusterCount++;
|
||||
}
|
||||
}
|
||||
if (linkedClusterCount > 1) {
|
||||
const basicLicenseCount = clusters.reduce((accumCount, cluster) => {
|
||||
if (cluster.license && cluster.license.type === 'basic') {
|
||||
accumCount++;
|
||||
|
@ -90,8 +100,8 @@ export function flagSupportedClusters(req, kbnIndexPattern) {
|
|||
return flagAllSupported(clusters);
|
||||
}
|
||||
|
||||
// if all basic licenses
|
||||
if (clusters.length === basicLicenseCount) {
|
||||
// if all linked are basic licenses
|
||||
if (linkedClusterCount === basicLicenseCount) {
|
||||
const kibanaUuid = config.get('server.uuid');
|
||||
return await findSupportedBasicLicenseCluster(req, clusters, kbnIndexPattern, kibanaUuid, serverLog);
|
||||
}
|
||||
|
|
|
@ -17,9 +17,10 @@ import { alertsClustersAggregation } from '../../cluster_alerts/alerts_clusters_
|
|||
import { alertsClusterSearch } from '../../cluster_alerts/alerts_cluster_search';
|
||||
import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license';
|
||||
import { getClustersSummary } from './get_clusters_summary';
|
||||
import { CLUSTER_ALERTS_SEARCH_SIZE } from '../../../common/constants';
|
||||
import { CLUSTER_ALERTS_SEARCH_SIZE, STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants';
|
||||
import { getApmsForClusters } from '../apm/get_apms_for_clusters';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { standaloneClusterDefinition, hasStandaloneClusters } from '../standalone_clusters';
|
||||
|
||||
/**
|
||||
* Get all clusters or the cluster associated with {@code clusterUuid} when it is defined.
|
||||
|
@ -34,8 +35,29 @@ export async function getClustersFromRequest(req, indexPatterns, { clusterUuid,
|
|||
alertsIndex
|
||||
} = indexPatterns;
|
||||
|
||||
// get clusters with stats and cluster state
|
||||
let clusters = await getClustersStats(req, esIndexPattern, clusterUuid);
|
||||
const isStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID;
|
||||
|
||||
let clusters = [];
|
||||
|
||||
if (isStandaloneCluster) {
|
||||
clusters.push(standaloneClusterDefinition);
|
||||
}
|
||||
else {
|
||||
// get clusters with stats and cluster state
|
||||
clusters = await getClustersStats(req, esIndexPattern, clusterUuid);
|
||||
}
|
||||
|
||||
if (!clusterUuid && !isStandaloneCluster) {
|
||||
const indexPatternsToCheckForNonClusters = [
|
||||
lsIndexPattern,
|
||||
beatsIndexPattern,
|
||||
apmIndexPattern
|
||||
];
|
||||
|
||||
if (await hasStandaloneClusters(req, indexPatternsToCheckForNonClusters)) {
|
||||
clusters.push(standaloneClusterDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this handling logic should be two different functions
|
||||
if (clusterUuid) { // if is defined, get specific cluster (no need for license checking)
|
||||
|
@ -63,7 +85,7 @@ export async function getClustersFromRequest(req, indexPatterns, { clusterUuid,
|
|||
if (alerts) {
|
||||
cluster.alerts = alerts;
|
||||
}
|
||||
} else {
|
||||
} else if (!isStandaloneCluster) {
|
||||
// get all clusters
|
||||
if (!clusters || clusters.length === 0) {
|
||||
// we do NOT throw 404 here so that the no-data page can use this to check for data
|
||||
|
@ -89,7 +111,7 @@ export async function getClustersFromRequest(req, indexPatterns, { clusterUuid,
|
|||
}
|
||||
|
||||
// add kibana data
|
||||
const kibanas = await getKibanasForClusters(req, kbnIndexPattern, clusters);
|
||||
const kibanas = isStandaloneCluster ? [] : await getKibanasForClusters(req, kbnIndexPattern, clusters);
|
||||
// add the kibana data to each cluster
|
||||
kibanas.forEach(kibana => {
|
||||
const clusterIndex = findIndex(clusters, { cluster_uuid: kibana.clusterUuid });
|
||||
|
|
|
@ -22,7 +22,7 @@ export function getClustersSummary(clusters, kibanaUuid) {
|
|||
apm,
|
||||
alerts,
|
||||
ccs,
|
||||
cluster_settings: clusterSettings
|
||||
cluster_settings: clusterSettings,
|
||||
} = cluster;
|
||||
|
||||
const clusterName = get(clusterSettings, 'cluster.metadata.display_name', cluster.cluster_name);
|
||||
|
@ -73,11 +73,11 @@ export function getClustersSummary(clusters, kibanaUuid) {
|
|||
beats,
|
||||
apm,
|
||||
alerts,
|
||||
isPrimary: kibana.uuids.includes(kibanaUuid),
|
||||
isPrimary: kibana ? kibana.uuids.includes(kibanaUuid) : false,
|
||||
status: calculateOverallStatus([
|
||||
status,
|
||||
kibana && kibana.status || null
|
||||
])
|
||||
]),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import { defaults, get } from 'lodash';
|
||||
import { MissingRequiredError } from './error_missing_required';
|
||||
import moment from 'moment';
|
||||
import { standaloneClusterFilter } from './standalone_clusters';
|
||||
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants';
|
||||
|
||||
/*
|
||||
* Builds a type filter syntax that supports backwards compatibility to read
|
||||
|
@ -46,13 +48,15 @@ export function createQuery(options) {
|
|||
options = defaults(options, { filters: [] });
|
||||
const { type, clusterUuid, uuid, start, end, filters } = options;
|
||||
|
||||
const isFromStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID;
|
||||
|
||||
let typeFilter;
|
||||
if (type) {
|
||||
typeFilter = createTypeFilter(type);
|
||||
}
|
||||
|
||||
let clusterUuidFilter;
|
||||
if (clusterUuid) {
|
||||
if (clusterUuid && !isFromStandaloneCluster) {
|
||||
clusterUuidFilter = { term: { 'cluster_uuid': clusterUuid } };
|
||||
}
|
||||
|
||||
|
@ -89,9 +93,15 @@ export function createQuery(options) {
|
|||
combinedFilters.push(timeRangeFilter);
|
||||
}
|
||||
|
||||
return {
|
||||
if (isFromStandaloneCluster) {
|
||||
combinedFilters.push(standaloneClusterFilter);
|
||||
}
|
||||
|
||||
const query = {
|
||||
bool: {
|
||||
filter: combinedFilters.filter(Boolean)
|
||||
}
|
||||
};
|
||||
|
||||
return query;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import { get } from 'lodash';
|
||||
import { standaloneClusterFilter } from './';
|
||||
|
||||
export async function hasStandaloneClusters(req, indexPatterns) {
|
||||
const indexPatternList = indexPatterns.reduce((list, patterns) => {
|
||||
list.push(...patterns.split(','));
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
const filters = [standaloneClusterFilter];
|
||||
// Not every page will contain a time range so check for that
|
||||
if (req.payload.timeRange) {
|
||||
const start = req.payload.timeRange.min;
|
||||
const end = req.payload.timeRange.max;
|
||||
|
||||
const timeRangeFilter = {
|
||||
range: {
|
||||
timestamp: {
|
||||
format: 'epoch_millis'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (start) {
|
||||
timeRangeFilter.range.timestamp.gte = moment.utc(start).valueOf();
|
||||
}
|
||||
if (end) {
|
||||
timeRangeFilter.range.timestamp.lte = moment.utc(end).valueOf();
|
||||
}
|
||||
filters.push(timeRangeFilter);
|
||||
}
|
||||
|
||||
const params = {
|
||||
index: indexPatternList,
|
||||
body: {
|
||||
size: 0,
|
||||
terminate_after: 1,
|
||||
query: {
|
||||
bool: {
|
||||
filter: filters,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
|
||||
const response = await callWithRequest(req, 'search', params);
|
||||
if (response && response.hits) {
|
||||
return get(response, 'hits.total.value', 0) > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { hasStandaloneClusters } from './has_standalone_clusters';
|
||||
export { standaloneClusterDefinition } from './standalone_cluster_definition';
|
||||
export { standaloneClusterFilter } from './standalone_cluster_query_filter';
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants';
|
||||
|
||||
export const standaloneClusterDefinition = {
|
||||
cluster_uuid: STANDALONE_CLUSTER_CLUSTER_UUID,
|
||||
license: {},
|
||||
cluster_state: {},
|
||||
cluster_stats: {
|
||||
nodes: {
|
||||
jvm: {},
|
||||
count: {}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const standaloneClusterFilter = {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
cluster_uuid: {
|
||||
value: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: 'cluster_uuid'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -18,7 +18,7 @@ export {
|
|||
} from './beats';
|
||||
export {
|
||||
clusterRoute,
|
||||
clustersRoute
|
||||
clustersRoute,
|
||||
} from './cluster';
|
||||
export {
|
||||
esIndexRoute,
|
||||
|
|
|
@ -14,5 +14,6 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./kibana'));
|
||||
loadTestFile(require.resolve('./logstash'));
|
||||
loadTestFile(require.resolve('./common'));
|
||||
loadTestFile(require.resolve('./standalone_cluster'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import clusterFixture from './fixtures/cluster';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('cluster', () => {
|
||||
const archive = 'monitoring/standalone_cluster';
|
||||
const timeRange = {
|
||||
min: '2019-01-15T19:00:49.104Z',
|
||||
max: '2019-01-15T19:59:49.104Z'
|
||||
};
|
||||
|
||||
before('load archive', () => {
|
||||
return esArchiver.load(archive);
|
||||
});
|
||||
|
||||
after('unload archive', () => {
|
||||
return esArchiver.unload(archive);
|
||||
});
|
||||
|
||||
it('should get cluster data', async () => {
|
||||
const { body } = await supertest
|
||||
.post('/api/monitoring/v1/clusters/__standalone_cluster__')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ timeRange })
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql(clusterFixture);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import clustersFixture from './fixtures/clusters';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('clusters', () => {
|
||||
const archive = 'monitoring/standalone_cluster';
|
||||
const timeRange = {
|
||||
min: '2019-01-15T19:00:49.104Z',
|
||||
max: '2019-01-15T19:59:49.104Z'
|
||||
};
|
||||
|
||||
before('load archive', () => {
|
||||
return esArchiver.load(archive);
|
||||
});
|
||||
|
||||
after('unload archive', () => {
|
||||
return esArchiver.unload(archive);
|
||||
});
|
||||
|
||||
it('should get the cluster listing', async () => {
|
||||
const { body } = await supertest
|
||||
.post('/api/monitoring/v1/clusters')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ timeRange })
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql(clustersFixture);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
[{"isSupported":true,"cluster_uuid":"__standalone_cluster__","license":{},"elasticsearch":{"cluster_stats":{"indices":{},"nodes":{"count":{},"jvm":{}}}},"logstash":{},"kibana":{"status":null,"requests_total":0,"concurrent_connections":0,"response_time_max":0,"memory_size":0,"memory_limit":0,"count":0},"beats":{"totalEvents":6963,"bytesSent":6283358,"beats":{"total":1,"types":[{"type":"Packetbeat","count":1}]}},"apm":{"totalEvents":0,"memRss":0,"memTotal":0,"apms":{"total":0}},"alerts":{"message":"Cluster Alerts are not displayed because the [production] cluster's license could not be determined."},"isPrimary":false}]
|
|
@ -0,0 +1 @@
|
|||
[{"cluster_uuid":"BsqrVriJSu21Q-MkOr6vTA","cluster_name":"monitoring","version":"7.0.0","license":{"status":"active","type":"basic"},"elasticsearch":{"cluster_stats":{"indices":{"count":5,"docs":{"count":7814,"deleted":169},"shards":{"total":7,"primaries":7,"replication":0,"index":{"shards":{"min":1,"max":3,"avg":1.4},"primaries":{"min":1,"max":3,"avg":1.4},"replication":{"min":0,"max":0,"avg":0}}},"store":{"size_in_bytes":9230231}},"nodes":{"fs":{"total_in_bytes":499963174912,"free_in_bytes":83429146624,"available_in_bytes":70893522944},"count":{"total":1},"jvm":{"max_uptime_in_millis":190074,"mem":{"heap_used_in_bytes":114044640,"heap_max_in_bytes":1038876672}}},"status":"yellow"}},"logstash":{"node_count":0,"events_in_total":0,"events_out_total":0,"avg_memory":0,"avg_memory_used":0,"max_uptime":0,"pipeline_count":0,"queue_types":{"memory":0,"persisted":0},"versions":[]},"kibana":{"status":"green","requests_total":3,"concurrent_connections":2,"response_time_max":58,"memory_size":255426560,"memory_limit":8564343808,"count":1},"beats":{"totalEvents":0,"bytesSent":0,"beats":{"total":0,"types":[]}},"apm":{"totalEvents":0,"memRss":0,"memTotal":0,"apms":{"total":0}},"alerts":{"alertsMeta":{"enabled":true},"clusterMeta":{"enabled":false,"message":"Cluster [monitoring] license type [basic] does not support Cluster Alerts"}},"isPrimary":true,"isSupported": true,"status":"yellow"},{"isSupported":true,"cluster_uuid":"__standalone_cluster__","license":{},"elasticsearch":{"cluster_stats":{"indices":{},"nodes":{"count":{},"jvm":{}}}},"logstash":{"node_count":0,"events_in_total":0,"events_out_total":0,"avg_memory":0,"avg_memory_used":0,"max_uptime":0,"pipeline_count":0,"queue_types":{"memory":0,"persisted":0},"versions":[]},"kibana":{"status":null,"requests_total":0,"concurrent_connections":0,"response_time_max":0,"memory_size":0,"memory_limit":0,"count":0},"beats":{"totalEvents":6963,"bytesSent":6283358,"beats":{"total":1,"types":[{"type":"Packetbeat","count":1}]}},"apm":{"totalEvents":0,"memRss":0,"memTotal":0,"apms":{"total":0}},"alerts":{"alertsMeta":{"enabled":true},"clusterMeta":{"enabled":false,"message":"Cluster [] license type [undefined] does not support Cluster Alerts"}},"isPrimary":false}]
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 default function ({ loadTestFile }) {
|
||||
describe('Standalone Cluster', () => {
|
||||
loadTestFile(require.resolve('./clusters'));
|
||||
loadTestFile(require.resolve('./cluster'));
|
||||
});
|
||||
}
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue