[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 commit 0e2e7608c3.

* Revert "Update loc pattern"

This reverts commit cc99fe8a8a.

* 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:
Chris Roberson 2019-01-25 09:34:01 -05:00 committed by GitHub
parent 50ec75f800
commit 9f37c1fd0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 2643 additions and 115 deletions

View file

@ -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__';

View file

@ -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';

View file

@ -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>
&nbsp;
<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"/>
&nbsp;
<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>
);
}
}

View file

@ -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} />

View file

@ -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';

View file

@ -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');

View file

@ -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>

View file

@ -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>
);
});
}
}
})

View file

@ -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.'
);
});
});
});
});
});

View file

@ -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);
}

View file

@ -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 });

View file

@ -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
])
]),
};
});
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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: {}
}
}
};

View file

@ -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'
}
}
]
}
}
]
}
};

View file

@ -18,7 +18,7 @@ export {
} from './beats';
export {
clusterRoute,
clustersRoute
clustersRoute,
} from './cluster';
export {
esIndexRoute,

View file

@ -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'));
});
}

View file

@ -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);
});
});
}

View file

@ -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);
});
});
}

View file

@ -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}]

View file

@ -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}]

View file

@ -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'));
});
}

File diff suppressed because it is too large Load diff