[Monitoring] Metricbeat migration flyout and instructions (#35228)

* Initial attempt at a reactor of how this works

* Enter and exiting setup mode with migration buttons working

* Adding monitoring url step back in and some small cleanup

* Elasticsearch steps

* Add missing file

* Better organization here

* Remove this debug logic

* Clean up

* PR feedback

* Add in monospacing

* Persist monitoring url in local storage

* Rework the steps

* Change node to server, and add missing files

* Fix linting issues

* Fix api integration tests

* PR feedback

* Pass down if the product is the "primary" or not, then use that to show certain warnings in the UI (just supported for Kibana right now)

* Elasticsearch migration will work slightly differently in that all nodes must be partially migrated before we can disable internal collection

* More PR feedback

* PR feedback

* Better links

* Fix tests

* This should open in a new tab

* PR feedback

* Design and PR feedback

* Fix these tests

* PR feedback

* Remove debug

* PR feedback

* Update the import path

* Update this import path too

* PR feedback

* Fix i18n
This commit is contained in:
Chris Roberson 2019-06-14 14:21:53 -04:00 committed by GitHub
parent cd76109a2c
commit b09e3a622f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2200 additions and 190 deletions

View file

@ -165,6 +165,8 @@ export const INDEX_PATTERN_FILEBEAT = 'filebeat-*';
// This is the unique token that exists in monitoring indices collected by metricbeat
export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-';
// We use this for metricbeat migration to identify specific products that we do not have constants for
export const ELASTICSEARCH_CUSTOM_ID = 'elasticsearch';
/**
* The id of the infra source owned by the monitoring plugin.
*/

View file

@ -75,6 +75,7 @@ const getProps = (field) => {
upTime: 28056934,
version: ['8.0.0']
},
setupMode: {},
nodes,
sorting: {
sort: { field: 'name', direction: 'asc' }

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 { NodeStatusIcon } from '../node';
import { extractIp } from '../../../lib/extract_ip'; // TODO this is only used for elasticsearch nodes summary / node detail, so it should be moved to components/elasticsearch/nodes/lib
import { ClusterStatus } from '../cluster_status';
@ -18,6 +18,8 @@ import {
EuiPageContent,
EuiPageBody,
EuiPanel,
EuiCallOut,
EuiButton,
EuiText
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -207,7 +209,37 @@ const getColumns = showCgroupMetricsElasticsearch => {
export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElasticsearch, ...props }) {
const columns = getColumns(showCgroupMetricsElasticsearch);
const { sorting, pagination, onTableChange } = props;
const { sorting, pagination, onTableChange, setupMode } = props;
let disableInternalCollectionForMigrationMessage = null;
if (setupMode.data) {
if (setupMode.data.totalUniquePartiallyMigratedCount === setupMode.data.totalUniqueInstanceCount) {
disableInternalCollectionForMigrationMessage = (
<Fragment>
<EuiCallOut
title={i18n.translate('xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionTitle', {
defaultMessage: 'Disable internal collection to finish the migration',
})}
color="warning"
iconType="help"
>
<p>
{i18n.translate('xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionDescription', {
defaultMessage: `All of your Elasticsearch servers are monitored using Metricbeat,
but you need to disable internal collection to finish the migration.`
})}
</p>
<EuiButton onClick={() => setupMode.openFlyout()} size="s" color="warning" fill>
{i18n.translate('xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionMigrationButtonLabel', {
defaultMessage: 'Disable and finish migration'
})}
</EuiButton>
</EuiCallOut>
<EuiSpacer size="m"/>
</Fragment>
);
}
}
return (
<EuiPage>
@ -216,6 +248,7 @@ export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElas
<ClusterStatus stats={clusterStatus} />
</EuiPanel>
<EuiSpacer size="m" />
{disableInternalCollectionForMigrationMessage}
<EuiPageContent>
<EuiMonitoringTable
className="elasticsearchNodesTable"
@ -223,6 +256,9 @@ export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElas
columns={columns}
sorting={sorting}
pagination={pagination}
setupMode={setupMode}
uuidField="resolver"
nameField="name"
search={{
box: {
incremental: true,

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 { KibanaInstances } from './instances';

View file

@ -0,0 +1,171 @@
/*
* 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 React, { PureComponent } from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiPanel, EuiSpacer, EuiLink } from '@elastic/eui';
import { capitalize } from 'lodash';
import { ClusterStatus } from '../cluster_status';
import { EuiMonitoringTable } from '../../table';
import { KibanaStatusIcon } from '../status_icon';
import { formatMetric, formatNumber } from '../../../lib/format_number';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
const getColumns = (kbnUrl, scope) => {
const columns = [
{
name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', {
defaultMessage: 'Name'
}),
field: 'name',
render: (name, kibana) => (
<EuiLink
onClick={() => {
scope.$evalAsync(() => {
kbnUrl.changePath(`/kibana/instances/${kibana.kibana.uuid}`);
});
}}
data-test-subj={`kibanaLink-${name}`}
>
{ name }
</EuiLink>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', {
defaultMessage: 'Status'
}),
field: 'status',
render: (status, kibana) => (
<div
title={`Instance status: ${status}`}
className="monTableCell__status"
>
<KibanaStatusIcon status={status} availability={kibana.availability} />&nbsp;
{ !kibana.availability ? (
<FormattedMessage
id="xpack.monitoring.kibana.listing.instanceStatus.offlineLabel"
defaultMessage="Offline"
/>
) : capitalize(status) }
</div>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.loadAverageColumnTitle', {
defaultMessage: 'Load Average'
}),
field: 'os.load.1m',
render: value => (
<span>
{formatMetric(value, '0.00')}
</span>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.memorySizeColumnTitle', {
defaultMessage: 'Memory Size'
}),
field: 'process.memory.resident_set_size_in_bytes',
render: value => (
<span>
{formatNumber(value, 'byte')}
</span>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.requestsColumnTitle', {
defaultMessage: 'Requests'
}),
field: 'requests.total',
render: value => (
<span>
{formatNumber(value, 'int_commas')}
</span>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.responseTimeColumnTitle', {
defaultMessage: 'Response Times'
}),
// It is possible this does not exist through MB collection
field: 'response_times.average',
render: (value, kibana) => {
if (!value) {
return null;
}
return (
<div>
<div className="monTableCell__splitNumber">
{ (formatNumber(value, 'int_commas') + ' ms avg') }
</div>
<div className="monTableCell__splitNumber">
{ formatNumber(kibana.response_times.max, 'int_commas') } ms max
</div>
</div>
);
}
}
];
return columns;
};
export class KibanaInstances extends PureComponent {
render() {
const {
instances,
clusterStatus,
angular,
setupMode,
sorting,
pagination,
onTableChange
} = this.props;
const dataFlattened = instances.map(item => ({
...item,
name: item.kibana.name,
status: item.kibana.status,
}));
return (
<EuiPage>
<EuiPageBody>
<EuiPanel>
<ClusterStatus stats={clusterStatus} />
</EuiPanel>
<EuiSpacer size="m" />
<EuiPageContent>
<EuiMonitoringTable
className="kibanaInstancesTable"
rows={dataFlattened}
columns={getColumns(angular.kbnUrl, angular.$scope)}
sorting={sorting}
pagination={pagination}
setupMode={setupMode}
uuidField="kibana.uuid"
nameField="name"
search={{
box: {
incremental: true,
placeholder: i18n.translate('xpack.monitoring.kibana.listing.filterInstancesPlaceholder', {
defaultMessage: 'Filter Instances…'
})
},
}}
onTableChange={onTableChange}
executeQueryOptions={{
defaultFields: ['name']
}}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}

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 const INSTRUCTION_STEP_SET_MONITORING_URL = 'setMonitoringUrl';
export const INSTRUCTION_STEP_ENABLE_METRICBEAT = 'enableMetricbeat';
export const INSTRUCTION_STEP_DISABLE_INTERNAL = 'disableInternal';

View file

@ -0,0 +1,328 @@
/*
* 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 React, { Fragment, Component } from 'react';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiTitle,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiButton,
EuiSteps,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiLink,
EuiText,
} from '@elastic/eui';
import { getInstructionSteps } from '../instruction_steps';
import { Storage } from '../../../../../../../src/legacy/ui/public/storage/storage';
import { STORAGE_KEY, ELASTICSEARCH_CUSTOM_ID } from '../../../../common/constants';
import { ensureMinimumTime } from '../../../lib/ensure_minimum_time';
import { i18n } from '@kbn/i18n';
import {
INSTRUCTION_STEP_SET_MONITORING_URL,
INSTRUCTION_STEP_ENABLE_METRICBEAT,
INSTRUCTION_STEP_DISABLE_INTERNAL
} from '../constants';
import { KIBANA_SYSTEM_ID } from '../../../../../telemetry/common/constants';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
const storage = new Storage(window.localStorage);
const ES_MONITORING_URL_KEY = `${STORAGE_KEY}.mb_migration.esMonitoringUrl`;
const AUTO_CHECK_INTERVAL_IN_MS = 5000;
const DEFAULT_ES_MONITORING_URL = 'http://localhost:9200';
export class Flyout extends Component {
constructor(props) {
super(props);
let esMonitoringUrl = storage.get(ES_MONITORING_URL_KEY);
if (!esMonitoringUrl) {
esMonitoringUrl = props.monitoringHosts ? props.monitoringHosts[0] : DEFAULT_ES_MONITORING_URL;
}
this.checkInterval = null;
let activeStep = INSTRUCTION_STEP_SET_MONITORING_URL;
if (props.product && props.product.isPartiallyMigrated) {
activeStep = INSTRUCTION_STEP_DISABLE_INTERNAL;
}
this.state = {
activeStep,
esMonitoringUrl,
checkedStatusByStep: {
[INSTRUCTION_STEP_ENABLE_METRICBEAT]: false,
[INSTRUCTION_STEP_DISABLE_INTERNAL]: false,
},
checkingMigrationStatus: false,
};
}
componentWillUpdate(_nextProps, nextState) {
// We attempt to provide a better UX for the user by automatically rechecking
// the status of their current step, once they have initiated a check manually.
// The logic here aims to remove the recheck one they have moved on from the
// step
const thisActiveStep = this.state.activeStep;
const nextActiveStep = nextState.activeStep;
const nextEnableMbStatus = nextState.checkedStatusByStep[INSTRUCTION_STEP_ENABLE_METRICBEAT];
const nowEnableMbStatus = this.state.checkedStatusByStep[INSTRUCTION_STEP_ENABLE_METRICBEAT];
const nextDisableInternalStatus = nextState.checkedStatusByStep[INSTRUCTION_STEP_DISABLE_INTERNAL];
const nowDisableInternalStatus = this.state.checkedStatusByStep[INSTRUCTION_STEP_DISABLE_INTERNAL];
const setupInterval = (nextEnableMbStatus && !nowEnableMbStatus) || (nextDisableInternalStatus && !nowDisableInternalStatus);
const removeInterval = thisActiveStep !== nextActiveStep;
if (removeInterval) {
clearInterval(this.checkInterval);
this.clearInterval = null;
}
if (setupInterval) {
this.checkInterval = setInterval(async () => {
await this.checkForMigrationStatus();
}, AUTO_CHECK_INTERVAL_IN_MS);
}
}
componentWillUnmount() {
clearInterval(this.checkInterval);
}
checkForMigrationStatus = async () => {
this.setState({ checkingMigrationStatus: true });
await ensureMinimumTime(this.props.updateProduct(), 1000);
this.setState(state => ({
...state,
checkingMigrationStatus: false,
checkedStatusByStep: {
...state.checkedStatusByStep,
[this.state.activeStep]: true,
}
}));
}
setEsMonitoringUrl = esMonitoringUrl => {
storage.set(ES_MONITORING_URL_KEY, esMonitoringUrl);
this.setState({ esMonitoringUrl });
}
renderActiveStep() {
const { product, productName, onClose, meta } = this.props;
const {
activeStep,
esMonitoringUrl,
checkedStatusByStep,
checkingMigrationStatus,
} = this.state;
switch (activeStep) {
case INSTRUCTION_STEP_SET_MONITORING_URL:
return (
<EuiForm>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.monitoring.metricbeatMigration.flyout.step1.monitoringUrlLabel', {
defaultMessage: 'Monitoring cluster URL'
})}
helpText={i18n.translate('xpack.monitoring.metricbeatMigration.flyout.step1.monitoringUrlHelpText', {
defaultMessage: `This is typically a single instance, but if you have multiple, enter all of instance urls comma-separated.
Keep in mind that the running metricbeat instance will need to be able to communicate with these Elasticsearch servers.`
})}
>
<EuiFieldText
fullWidth
value={esMonitoringUrl}
onChange={e => this.setEsMonitoringUrl(e.target.value)}
/>
</EuiFormRow>
</EuiForm>
);
case INSTRUCTION_STEP_ENABLE_METRICBEAT:
case INSTRUCTION_STEP_DISABLE_INTERNAL:
const instructionSteps = getInstructionSteps(productName, product, activeStep, meta, {
doneWithMigration: onClose,
esMonitoringUrl,
checkForMigrationStatus: this.checkForMigrationStatus,
checkingMigrationStatus,
hasCheckedStatus: checkedStatusByStep[activeStep],
autoCheckIntervalInMs: AUTO_CHECK_INTERVAL_IN_MS,
});
return (
<Fragment>
<EuiSteps steps={instructionSteps}/>
</Fragment>
);
}
return null;
}
renderActiveStepNextButton() {
const { product, productName } = this.props;
const { activeStep, esMonitoringUrl } = this.state;
// It is possible that, during the migration steps, products are not reporting
// monitoring data for a period of time outside the window of our server-side check
// and this is most likely temporary so we want to be defensive and not error out
// and hopefully wait for the next check and this state will be self-corrected.
if (!product) {
return null;
}
let willDisableDoneButton = !product.isFullyMigrated;
let willShowNextButton = activeStep !== INSTRUCTION_STEP_DISABLE_INTERNAL;
if (activeStep === INSTRUCTION_STEP_ENABLE_METRICBEAT && productName === ELASTICSEARCH_CUSTOM_ID) {
willShowNextButton = false;
willDisableDoneButton = !product.isPartiallyMigrated;
}
if (willShowNextButton) {
let isDisabled = false;
let nextStep = null;
if (activeStep === INSTRUCTION_STEP_SET_MONITORING_URL) {
isDisabled = !esMonitoringUrl || esMonitoringUrl.length === 0;
if (product.isPartiallyMigrated || product.isFullyMigrated) {
nextStep = INSTRUCTION_STEP_DISABLE_INTERNAL;
}
else {
nextStep = INSTRUCTION_STEP_ENABLE_METRICBEAT;
}
}
else if (activeStep === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
isDisabled = !product.isPartiallyMigrated && !product.isFullyMigrated;
nextStep = INSTRUCTION_STEP_DISABLE_INTERNAL;
}
return (
<EuiButton
type="submit"
fill
iconType="sortRight"
iconSide="right"
isDisabled={isDisabled}
onClick={() => this.setState({ activeStep: nextStep })}
>
{i18n.translate('xpack.monitoring.metricbeatMigration.flyout.nextButtonLabel', {
defaultMessage: 'Next'
})}
</EuiButton>
);
}
return (
<EuiButton
type="submit"
fill
isDisabled={willDisableDoneButton}
onClick={this.props.onClose}
>
{i18n.translate('xpack.monitoring.metricbeatMigration.flyout.doneButtonLabel', {
defaultMessage: 'Done'
})}
</EuiButton>
);
}
getDocumentationTitle() {
const { productName } = this.props;
let documentationUrl = null;
if (productName === KIBANA_SYSTEM_ID) {
documentationUrl = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`;
}
else if (productName === ELASTICSEARCH_CUSTOM_ID) {
documentationUrl = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html`;
}
if (!documentationUrl) {
return null;
}
return (
<EuiText size="s">
<EuiLink href={documentationUrl} target="_blank">
Read more about this migration.
</EuiLink>
</EuiText>
);
}
render() {
const { onClose, instance, productName } = this.props;
let instanceType = null;
let instanceName = instance ? instance.name : null;
if (productName === KIBANA_SYSTEM_ID) {
instanceType = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.kibanaInstance', {
defaultMessage: 'instance',
});
}
else if (productName === ELASTICSEARCH_CUSTOM_ID) {
if (instance) {
instanceType = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.elasticsearchNode', {
defaultMessage: 'node',
});
}
else {
instanceName = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.elasticsearchNodesTitle', {
defaultMessage: 'Elasticsearch nodes',
});
}
}
return (
<EuiFlyout
onClose={onClose}
aria-labelledby="flyoutTitle"
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="flyoutTitle">
{i18n.translate('xpack.monitoring.metricbeatMigration.flyout.flyoutTitle', {
defaultMessage: 'Migrate {instanceName} {instanceType} to Metricbeat',
values: {
instanceName,
instanceType
}
})}
</h2>
</EuiTitle>
{this.getDocumentationTitle()}
</EuiFlyoutHeader>
<EuiFlyoutBody>
{this.renderActiveStep()}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
onClick={onClose}
flush="left"
>
{i18n.translate('xpack.monitoring.metricbeatMigration.flyout.closeButtonLabel', {
defaultMessage: 'Close'
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{this.renderActiveStepNextButton()}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
}

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 { Flyout } from './flyout';

View file

@ -0,0 +1,8 @@
/*
* 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 { Monospace } from './monospace';

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.
*/
import React from 'react';
export const Monospace = ({ children }) => (
<span style={{ fontFamily: 'monospace' }}>{children}</span>
);

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const statusTitle = i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusTitle', {
defaultMessage: `Migration status`
});

View file

@ -0,0 +1,195 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import {
EuiSpacer,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiCallOut,
EuiText
} from '@elastic/eui';
import { formatTimestampToDuration } from '../../../../../common';
import { CALCULATE_DURATION_SINCE } from '../../../../../common/constants';
import { Monospace } from '../components/monospace';
import { FormattedMessage } from '@kbn/i18n/react';
import { statusTitle } from './common_elasticsearch_instructions';
export function getElasticsearchInstructionsForDisablingInternalCollection(product, meta, {
checkForMigrationStatus,
checkingMigrationStatus,
hasCheckedStatus,
autoCheckIntervalInMs,
}) {
const disableInternalCollectionStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollectionTitle', {
defaultMessage: 'Disable internal collection of Elasticsearch monitoring metrics'
}),
children: (
<Fragment>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollectionDescription"
defaultMessage="Disable internal collection of Elasticsearch monitoring metrics.
Set {monospace} to false on each server in the production cluster."
values={{
monospace: (
<Monospace>xpack.monitoring.elasticsearch.collection.enabled</Monospace>
)
}}
/>
</p>
</EuiText>
<EuiSpacer size="s"/>
<EuiCodeBlock
isCopyable
language="curl"
>
{`PUT _cluster/settings
{
"persistent": {
"xpack.monitoring.elasticsearch.collection.enabled": false
}
}
`}
</EuiCodeBlock>
</Fragment>
)
};
let migrationStatusStep = null;
if (!product || !product.isFullyMigrated) {
let status = null;
if (hasCheckedStatus) {
let lastInternallyCollectedMessage = '';
// It is possible that, during the migration steps, products are not reporting
// monitoring data for a period of time outside the window of our server-side check
// and this is most likely temporary so we want to be defensive and not error out
// and hopefully wait for the next check and this state will be self-corrected.
if (product) {
const lastInternallyCollectedTimestamp = product.lastInternallyCollectedTimestamp || product.lastTimestamp;
const secondsSinceLastInternalCollectionLabel =
formatTimestampToDuration(lastInternallyCollectedTimestamp, CALCULATE_DURATION_SINCE);
lastInternallyCollectedMessage = (<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.partiallyMigratedStatusDescription"
defaultMessage="Last internal collection occurred {secondsSinceLastInternalCollectionLabel} ago."
values={{
secondsSinceLastInternalCollectionLabel,
}}
/>);
}
status = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.partiallyMigratedStatusTitle',
{
defaultMessage: `We still see data coming from internal collection of Elasticsearch.`
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.partiallyMigratedStatusDescription"
defaultMessage="Note that it can take up to {secondsAgo} seconds to detect, but
we will continuously check every {timePeriod} seconds in the background."
values={{
secondsAgo: meta.secondsAgo,
timePeriod: autoCheckIntervalInMs / 1000,
}}
/>
</p>
<p>
{lastInternallyCollectedMessage}
</p>
</EuiCallOut>
</Fragment>
);
}
let buttonLabel;
if (checkingMigrationStatus) {
buttonLabel = i18n.translate(
'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.checkingStatusButtonLabel',
{
defaultMessage: 'Checking...'
}
);
} else {
buttonLabel = i18n.translate(
'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.checkStatusButtonLabel',
{
defaultMessage: 'Check'
}
);
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate(
'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.statusDescription',
{
defaultMessage: 'Check that no documents are coming from internal collection.'
}
)}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={checkForMigrationStatus} isDisabled={checkingMigrationStatus}>
{buttonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{status}
</Fragment>
)
};
}
else {
migrationStatusStep = {
title: statusTitle,
status: 'complete',
children: (
<EuiCallOut
size="s"
color="success"
title={i18n.translate(
'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.fullyMigratedStatusDescription"
defaultMessage="We are not seeing any documents from internal collection. Migration complete!"
/>
</p>
</EuiCallOut>
)
};
}
return [
disableInternalCollectionStep,
migrationStatusStep
];
}

View file

@ -0,0 +1,271 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import {
EuiSpacer,
EuiCodeBlock,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiCallOut,
EuiText
} from '@elastic/eui';
import { Monospace } from '../components/monospace';
import { FormattedMessage } from '@kbn/i18n/react';
import { statusTitle } from './common_elasticsearch_instructions';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta, {
esMonitoringUrl,
hasCheckedStatus,
checkingMigrationStatus,
checkForMigrationStatus,
autoCheckIntervalInMs
}) {
const securitySetup = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
color="warning"
iconType="help"
title={(
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.metricbeatSecuritySetup"
defaultMessage="If security features are enabled, there may be more setup required.{link}"
values={{
link: (
<Fragment>
{` `}
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.metricbeatSecuritySetupLinkText"
defaultMessage="View more information."
/>
</EuiLink>
</Fragment>
)
}}
/>
</EuiText>
)}
/>
</Fragment>
);
const installMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatTitle', {
defaultMessage: 'Install Metricbeat on the same server as Elasticsearch'
}),
children: (
<EuiText>
<p>
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
const enableMetricbeatModuleStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleTitle', {
defaultMessage: 'Enable and configure the Elasticsearch x-pack module in Metricbeat'
}),
children: (
<Fragment>
<EuiCodeBlock
isCopyable
language="bash"
>
metricbeat modules enable elasticsearch-xpack
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleDescription"
defaultMessage="By default the module will collect Elasticsearch monitoring metrics from {url}.
If the local Elasticsearch server has a different address,
you must specify it via the hosts setting in the {module} file."
values={{
module: (
<Monospace>modules.d/elasticsearch-xpack.yml</Monospace>
),
url: (
<Monospace>http://localhost:9200</Monospace>
)
}}
/>
</p>
</EuiText>
{securitySetup}
</Fragment>
)
};
const configureMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatTitle', {
defaultMessage: 'Configure Metricbeat to send to the monitoring cluster'
}),
children: (
<Fragment>
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatDescription"
defaultMessage="Make these changes in your {file}."
values={{
file: (
<Monospace>metricbeat.yml</Monospace>
)
}}
/>
</EuiText>
<EuiSpacer size="s"/>
<EuiCodeBlock
isCopyable
>
{`output.elasticsearch:
hosts: ["${esMonitoringUrl}"] ## Monitoring cluster
# Optional protocol and basic auth credentials.
#protocol: "https"
#username: "elastic"
#password: "changeme"
`}
</EuiCodeBlock>
{securitySetup}
</Fragment>
)
};
const startMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatTitle', {
defaultMessage: 'Start Metricbeat'
}),
children: (
<EuiText>
<p>
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
let migrationStatusStep = null;
if (product.isInternalCollector) {
let status = null;
if (hasCheckedStatus) {
status = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.isInternalCollectorStatusTitle', {
defaultMessage: `We have not detected any monitoring data coming from Metricbeat for this Elasticsearch.
We will continuously check every {timePeriod} seconds in the background.`,
values: {
timePeriod: autoCheckIntervalInMs / 1000,
}
})}
/>
</Fragment>
);
}
let buttonLabel;
if (checkingMigrationStatus) {
buttonLabel = i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.checkingStatusButtonLabel', {
defaultMessage: 'Checking for data...'
});
} else {
buttonLabel = i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.checkStatusButtonLabel', {
defaultMessage: 'Check for data'
});
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusDescription', {
defaultMessage: 'Check that data is received from the Metricbeat'
})}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={checkForMigrationStatus} isDisabled={checkingMigrationStatus}>
{buttonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{status}
</Fragment>
)
};
}
else if (product.isPartiallyMigrated || product.isFullyMigrated) {
migrationStatusStep = {
title: statusTitle,
status: 'complete',
children: (
<EuiCallOut
size="s"
color="success"
title={i18n.translate(
'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.fullyMigratedStatusDescription"
defaultMessage="We are now seeing monitoring data shipping from Metricbeat!"
/>
</p>
</EuiCallOut>
)
};
}
return [
installMetricbeatStep,
enableMetricbeatModuleStep,
configureMetricbeatStep,
startMetricbeatStep,
migrationStatusStep
];
}

View file

@ -0,0 +1,8 @@
/*
* 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 { getElasticsearchInstructionsForDisablingInternalCollection } from './disable_internal_collection_instructions';
export { getElasticsearchInstructionsForEnablingMetricbeat } from './enable_metricbeat_instructions';

View file

@ -0,0 +1,38 @@
/*
* 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 {
getKibanaInstructionsForEnablingMetricbeat,
getKibanaInstructionsForDisablingInternalCollection,
} from './kibana';
import {
getElasticsearchInstructionsForEnablingMetricbeat,
getElasticsearchInstructionsForDisablingInternalCollection
} from './elasticsearch';
import {
INSTRUCTION_STEP_ENABLE_METRICBEAT,
INSTRUCTION_STEP_DISABLE_INTERNAL
} from '../constants';
export function getInstructionSteps(productName, product, step, meta, opts) {
switch (productName) {
case 'kibana':
if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
return getKibanaInstructionsForEnablingMetricbeat(product, meta, opts);
}
if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) {
return getKibanaInstructionsForDisablingInternalCollection(product, meta, opts);
}
case 'elasticsearch':
if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
return getElasticsearchInstructionsForEnablingMetricbeat(product, meta, opts);
}
if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) {
return getElasticsearchInstructionsForDisablingInternalCollection(product, meta, opts);
}
}
return [];
}

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 * from './get_instruction_steps';

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const statusTitle = i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.statusTitle', {
defaultMessage: `Migration status`
});

View file

@ -0,0 +1,235 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import {
EuiSpacer,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiCallOut,
EuiText
} from '@elastic/eui';
import { formatTimestampToDuration } from '../../../../../common';
import { CALCULATE_DURATION_SINCE } from '../../../../../common/constants';
import { Monospace } from '../components/monospace';
import { FormattedMessage } from '@kbn/i18n/react';
import { statusTitle } from './common_kibana_instructions';
export function getKibanaInstructionsForDisablingInternalCollection(product, meta, {
checkForMigrationStatus,
checkingMigrationStatus,
hasCheckedStatus,
autoCheckIntervalInMs,
}) {
let restartWarning = null;
if (product.isPrimary) {
restartWarning = (
<Fragment>
<EuiSpacer size="s"/>
<EuiCallOut
title={i18n.translate(
'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.restartWarningTitle',
{
defaultMessage: 'Warning'
}
)}
color="warning"
iconType="help"
>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.restartNote"
defaultMessage="This step requires you to restart the Kibana server.
Expect to see errors until the server is running again."
/>
</p>
</EuiText>
</EuiCallOut>
</Fragment>
);
}
const disableInternalCollectionStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.title', {
defaultMessage: 'Disable internal collection of Kibana monitoring metrics'
}),
children: (
<Fragment>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.description"
defaultMessage="Add the following setting in the Kibana configuration file ({file}):"
values={{
file: (
<Monospace>kibana.yml</Monospace>
)
}}
/>
</p>
</EuiText>
<EuiSpacer size="s"/>
<EuiCodeBlock
isCopyable
language="bash"
>
xpack.monitoring.kibana.collection.enabled: false
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.note"
defaultMessage="Leave the {config} set to its default value ({defaultValue})."
values={{
config: (
<Monospace>xpack.monitoring.enabled</Monospace>
),
defaultValue: (
<Monospace>true</Monospace>
)
}}
/>
</p>
</EuiText>
{restartWarning}
</Fragment>
)
};
let migrationStatusStep = null;
if (!product || !product.isFullyMigrated) {
let status = null;
if (hasCheckedStatus) {
let lastInternallyCollectedMessage = '';
// It is possible that, during the migration steps, products are not reporting
// monitoring data for a period of time outside the window of our server-side check
// and this is most likely temporary so we want to be defensive and not error out
// and hopefully wait for the next check and this state will be self-corrected.
if (product) {
const lastInternallyCollectedTimestamp = product.lastInternallyCollectedTimestamp || product.lastTimestamp;
const secondsSinceLastInternalCollectionLabel =
formatTimestampToDuration(lastInternallyCollectedTimestamp, CALCULATE_DURATION_SINCE);
lastInternallyCollectedMessage = (<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.partiallyMigratedStatusDescription"
defaultMessage="Last internal collection occurred {secondsSinceLastInternalCollectionLabel} ago."
values={{
secondsSinceLastInternalCollectionLabel,
}}
/>);
}
status = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.partiallyMigratedStatusTitle',
{
defaultMessage: `We still see data coming from internal collection of Kibana.`
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.partiallyMigratedStatusDescription"
defaultMessage="Note that it can take up to {secondsAgo} seconds to detect, but
we will continuously check every {timePeriod} seconds in the background."
values={{
secondsAgo: meta.secondsAgo,
timePeriod: autoCheckIntervalInMs / 1000,
}}
/>
</p>
<p>
{lastInternallyCollectedMessage}
</p>
</EuiCallOut>
</Fragment>
);
}
let buttonLabel;
if (checkingMigrationStatus) {
buttonLabel = i18n.translate(
'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkingStatusButtonLabel',
{
defaultMessage: 'Checking...'
}
);
} else {
buttonLabel = i18n.translate(
'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkStatusButtonLabel',
{
defaultMessage: 'Check'
}
);
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate(
'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.statusDescription',
{
defaultMessage: 'Check that no documents are coming from internal collection.'
}
)}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={checkForMigrationStatus} isDisabled={checkingMigrationStatus}>
{buttonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{status}
</Fragment>
)
};
}
else {
migrationStatusStep = {
title: statusTitle,
status: 'complete',
children: (
<EuiCallOut
size="s"
color="success"
title={i18n.translate(
'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.fullyMigratedStatusDescription"
defaultMessage="We are not seeing any documents from internal collection. Migration complete!"
/>
</p>
</EuiCallOut>
)
};
}
return [
disableInternalCollectionStep,
migrationStatusStep
];
}

View file

@ -0,0 +1,267 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import {
EuiSpacer,
EuiCodeBlock,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiCallOut,
EuiText
} from '@elastic/eui';
import { Monospace } from '../components/monospace';
import { FormattedMessage } from '@kbn/i18n/react';
import { statusTitle } from './common_kibana_instructions';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, {
esMonitoringUrl,
hasCheckedStatus,
checkingMigrationStatus,
checkForMigrationStatus,
autoCheckIntervalInMs
}) {
const securitySetup = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
color="warning"
iconType="help"
title={(
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.metricbeatSecuritySetup"
defaultMessage="If security features are enabled, there may be more setup required.{link}"
values={{
link: (
<Fragment>
{` `}
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.metricbeatSecuritySetupLinkText"
defaultMessage="View more information."
/>
</EuiLink>
</Fragment>
)
}}
/>
</EuiText>
)}
/>
</Fragment>
);
const installMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatTitle', {
defaultMessage: 'Install Metricbeat on the same server as Kibana'
}),
children: (
<EuiText>
<p>
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
const enableMetricbeatModuleStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleTitle', {
defaultMessage: 'Enable and configure the Kibana x-pack module in Metricbeat'
}),
children: (
<Fragment>
<EuiCodeBlock
isCopyable
language="bash"
>
metricbeat modules enable kibana-xpack
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleDescription"
defaultMessage="By default the module will collect Kibana monitoring metrics from http://localhost:5601. If the local Kibana instance has a different address, you must specify it via the {hosts} setting in the {file} file."
values={{
hosts: (
<Monospace>hosts</Monospace>
),
file: (
<Monospace>modules.d/kibana-xpack.yml</Monospace>
)
}}
/>
</p>
</EuiText>
{securitySetup}
</Fragment>
)
};
const configureMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatTitle', {
defaultMessage: 'Configure Metricbeat to send to the monitoring cluster'
}),
children: (
<Fragment>
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatDescription"
defaultMessage="Make these changes in your {file}."
values={{
file: (
<Monospace>metricbeat.yml</Monospace>
)
}}
/>
</EuiText>
<EuiSpacer size="s"/>
<EuiCodeBlock
isCopyable
>
{`output.elasticsearch:
hosts: ["${esMonitoringUrl}"] ## Monitoring cluster
# Optional protocol and basic auth credentials.
#protocol: "https"
#username: "elastic"
#password: "changeme"
`}
</EuiCodeBlock>
{securitySetup}
</Fragment>
)
};
const startMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatTitle', {
defaultMessage: 'Start Metricbeat'
}),
children: (
<EuiText>
<p>
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
let migrationStatusStep = null;
if (product.isInternalCollector) {
let status = null;
if (hasCheckedStatus) {
status = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.isInternalCollectorStatusTitle', {
defaultMessage: `We have not detected any monitoring data coming from Metricbeat for this Kibana.
We will continuously check every {timePeriod} seconds in the background.`,
values: {
timePeriod: autoCheckIntervalInMs / 1000,
}
})}
/>
</Fragment>
);
}
let buttonLabel;
if (checkingMigrationStatus) {
buttonLabel = i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.checkingStatusButtonLabel', {
defaultMessage: 'Checking for data...'
});
} else {
buttonLabel = i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.checkStatusButtonLabel', {
defaultMessage: 'Check for data'
});
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.statusDescription', {
defaultMessage: 'Check that data is received from the Metricbeat'
})}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={checkForMigrationStatus} isDisabled={checkingMigrationStatus}>
{buttonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{status}
</Fragment>
)
};
}
else if (product.isPartiallyMigrated || product.isFullyMigrated) {
migrationStatusStep = {
title: statusTitle,
status: 'complete',
children: (
<EuiCallOut
size="s"
color="success"
title={i18n.translate(
'xpack.monitoring.metricbeatMigration.kibanaInstructions.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.kibanaInstructions.fullyMigratedStatusDescription"
defaultMessage="We are now seeing monitoring data shipping from Metricbeat!"
/>
</p>
</EuiCallOut>
)
};
}
return [
installMetricbeatStep,
enableMetricbeatModuleStep,
configureMetricbeatStep,
startMetricbeatStep,
migrationStatusStep
];
}

View file

@ -0,0 +1,8 @@
/*
* 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 { getKibanaInstructionsForDisablingInternalCollection } from './disable_internal_collection_instructions';
export { getKibanaInstructionsForEnablingMetricbeat } from './enable_metricbeat_instructions';

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 { SetupModeRenderer } from './setup_mode';

View file

@ -0,0 +1,68 @@
/*
* 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 React from 'react';
import { setAngularState, getSetupModeState, initSetupModeState, updateSetupModeData } from '../../lib/setup_mode';
import { Flyout } from '../metricbeat_migration/flyout';
import { ELASTICSEARCH_CUSTOM_ID } from '../../../common/constants';
export class SetupModeRenderer extends React.Component {
state = {
renderState: false,
isFlyoutOpen: false,
instance: null,
}
componentWillMount() {
const { scope, injector } = this.props;
setAngularState(scope, injector);
initSetupModeState(() => this.setState({ renderState: true }));
}
getFlyout(data, meta) {
const { productName } = this.props;
const { isFlyoutOpen, instance } = this.state;
if (!data || !isFlyoutOpen) {
return null;
}
let product = instance ? data.byUuid[instance.uuid] : null;
const isFullyOrPartiallyMigrated = data.totalUniquePartiallyMigratedCount === data.totalUniqueInstanceCount
|| data.totalUniqueFullyMigratedCount === data.totalUniqueInstanceCount;
if (!product && productName === ELASTICSEARCH_CUSTOM_ID && isFullyOrPartiallyMigrated) {
product = Object.values(data.byUuid)[0];
}
return (
<Flyout
onClose={() => this.setState({ isFlyoutOpen: false })}
productName={productName}
product={product}
meta={meta}
instance={instance}
updateProduct={updateSetupModeData}
/>
);
}
render() {
const { render, productName } = this.props;
const setupModeState = getSetupModeState();
const data = setupModeState.data ? setupModeState.data[productName] : null;
const meta = setupModeState.data ? setupModeState.data._meta : null;
return render({
setupMode: {
data,
enabled: setupModeState.enabled,
productName,
updateSetupModeData,
openFlyout: (instance) => this.setState({ isFlyoutOpen: true, instance }),
closeFlyout: () => this.setState({ isFlyoutOpen: false }),
},
flyoutComponent: this.getFlyout(data, meta),
});
}
}

View file

@ -5,9 +5,15 @@
*/
import React from 'react';
import { get } from 'lodash';
import {
EuiInMemoryTable
EuiInMemoryTable,
EuiBadge,
EuiButtonEmpty,
EuiHealth
} from '@elastic/eui';
import { ELASTICSEARCH_CUSTOM_ID } from '../../../common/constants';
import { i18n } from '@kbn/i18n';
export class EuiMonitoringTable extends React.PureComponent {
render() {
@ -15,6 +21,9 @@ export class EuiMonitoringTable extends React.PureComponent {
rows: items,
search = {},
columns: _columns,
setupMode,
uuidField,
nameField,
...props
} = this.props;
@ -37,6 +46,89 @@ export class EuiMonitoringTable extends React.PureComponent {
return column;
});
if (setupMode && setupMode.enabled) {
columns.push({
name: i18n.translate('xpack.monitoring.euiTable.setupStatusTitle', {
defaultMessage: 'Setup Status'
}),
field: uuidField,
render: (uuid) => {
const list = get(setupMode, 'data.byUuid', {});
const status = list[uuid] || {};
let statusBadge = null;
if (status.isInternalCollector) {
statusBadge = (
<EuiHealth color="danger">
{i18n.translate('xpack.monitoring.euiTable.isInternalCollectorLabel', {
defaultMessage: 'Internal collection'
})}
</EuiHealth>
);
}
else if (status.isPartiallyMigrated) {
statusBadge = (
<EuiHealth color="warning">
{i18n.translate('xpack.monitoring.euiTable.isPartiallyMigratedLabel', {
defaultMessage: 'Internal collection and Metricbeat collection'
})}
</EuiHealth>
);
}
else if (status.isFullyMigrated) {
statusBadge = (
<EuiBadge color="primary">
{i18n.translate('xpack.monitoring.euiTable.isFullyMigratedLabel', {
defaultMessage: 'Metricbeat collection'
})}
</EuiBadge>
);
}
else {
statusBadge = i18n.translate('xpack.monitoring.euiTable.migrationStatusUnknown', {
defaultMessage: 'N/A'
});
}
return statusBadge;
}
});
columns.push({
name: i18n.translate('xpack.monitoring.euiTable.setupActionTitle', {
defaultMessage: 'Setup Action'
}),
field: uuidField,
render: (uuid, product) => {
const list = get(setupMode, 'data.byUuid', {});
const status = list[uuid] || {};
const instance = {
uuid: get(product, uuidField),
name: get(product, nameField),
};
// Migrating from partially to fully for Elasticsearch involves changing a cluster
// setting which impacts all nodes in the cluster, which we have a separate callout
// for. Since it does not make sense to do this on a per node basis, show nothing here
if (status.isPartiallyMigrated && setupMode.productName === ELASTICSEARCH_CUSTOM_ID) {
return null;
}
if (status.isInternalCollector || status.isPartiallyMigrated) {
return (
<EuiButtonEmpty flush="left" size="s" color="primary" onClick={() => setupMode.openFlyout(instance)}>
{i18n.translate('xpack.monitoring.euiTable.migrateButtonLabel', {
defaultMessage: 'Migrate'
})}
</EuiButtonEmpty>
);
}
return null;
}
});
}
return (
<div data-test-subj={`${this.props.className}Container`}>
<EuiInMemoryTable

View file

@ -1,5 +1,5 @@
<div class="app-container">
<kbn-top-nav name="{{ monitoringMain.name }}-nav">
<kbn-top-nav name="{{ monitoringMain.name }}-nav" config="topNavMenu">
<!-- Transcluded elements. -->
<div data-transclude-slots>
<!-- Tabs -->
@ -204,10 +204,10 @@
i18n-default-message="Advanced"
>
</a>
<div
class="kuiLocalTab"
ng-if="monitoringMain.pipelineVersions.length"
id="dropdown-elm"
<div
class="kuiLocalTab"
ng-if="monitoringMain.pipelineVersions.length"
id="dropdown-elm"
ng-init="monitoringMain.dropdownLoadedHandler()">
</div>
</div>

View file

@ -0,0 +1,45 @@
/*
* 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.
*/
/**
* When you make an async request, typically you want to show the user a spinner while they wait.
* However, if the request takes less than 300 ms, the spinner will flicker in the UI and the user
* won't have time to register it as a spinner. This function ensures the spinner (or whatever
* you're showing the user) displays for at least 300 ms, even if the request completes before then.
*/
export const DEFAULT_MINIMUM_TIME_MS = 300;
export async function ensureMinimumTime(promiseOrPromises, minimumTimeMs = DEFAULT_MINIMUM_TIME_MS) {
let returnValue;
// https://kibana-ci.elastic.co/job/elastic+kibana+6.x+multijob-intake/128/console
// We're having periodic failures around the timing here. I'm not exactly sure
// why it's not consistent but I'm going to add some buffer space here to
// prevent these random failures
const bufferedMinimumTimeMs = minimumTimeMs + 5;
// Block on the async action and start the clock.
const asyncActionStartTime = new Date().getTime();
if (Array.isArray(promiseOrPromises)) {
returnValue = await Promise.all(promiseOrPromises);
} else {
returnValue = await promiseOrPromises;
}
// Measure how long the async action took to complete.
const asyncActionCompletionTime = new Date().getTime();
const asyncActionDuration = asyncActionCompletionTime - asyncActionStartTime;
// Wait longer if the async action completed too quickly.
if (asyncActionDuration < bufferedMinimumTimeMs) {
const additionalWaitingTime = bufferedMinimumTimeMs - (asyncActionCompletionTime - asyncActionStartTime);
await new Promise(resolve => setTimeout(resolve, additionalWaitingTime));
}
return returnValue;
}

View file

@ -0,0 +1,34 @@
/*
* 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 { ensureMinimumTime } from './ensure_minimum_time';
describe('ensureMinimumTime', () => {
it('resolves single promise', async (done) => {
const promiseA = new Promise(resolve => resolve('a'));
const a = await ensureMinimumTime(promiseA, 0);
expect(a).toBe('a');
done();
});
it('resolves multiple promises', async (done) => {
const promiseA = new Promise(resolve => resolve('a'));
const promiseB = new Promise(resolve => resolve('b'));
const [ a, b ] = await ensureMinimumTime([promiseA, promiseB], 0);
expect(a).toBe('a');
expect(b).toBe('b');
done();
});
it('resolves in the amount of time provided, at minimum', async (done) => {
const startTime = new Date().getTime();
const promise = new Promise(resolve => resolve());
await ensureMinimumTime(promise, 100);
const endTime = new Date().getTime();
expect(endTime - startTime).toBeGreaterThanOrEqual(100);
done();
});
});

View file

@ -0,0 +1,126 @@
/*
* 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 { ajaxErrorHandlersProvider } from './ajax_error_handler';
const angularState = {
injector: null,
scope: null,
};
export const setAngularState = ($scope, $injector) => {
angularState.scope = $scope;
angularState.injector = $injector;
};
const checkAngularState = () => {
if (!angularState.injector || !angularState.scope) {
throw 'Unable to interact with setup mode because the angular injector was not previously set.'
+ ' This needs to be set by calling `setAngularState`.';
}
};
const setupModeState = {
enabled: false,
data: null,
callbacks: []
};
export const getSetupModeState = () => setupModeState;
export const fetchCollectionData = async () => {
checkAngularState();
const http = angularState.injector.get('$http');
const globalState = angularState.injector.get('globalState');
const clusterUuid = globalState.cluster_uuid;
const ccs = globalState.ccs;
let url = '../api/monitoring/v1/setup/collection';
if (clusterUuid) {
url += `/${clusterUuid}`;
}
try {
const response = await http.post(url, { ccs });
return response.data;
}
catch (err) {
const Private = angularState.injector.get('Private');
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);
}
};
const notifySetupModeDataChange = () => {
setupModeState.callbacks.forEach(cb => cb());
};
export const updateSetupModeData = async () => {
setupModeState.data = await fetchCollectionData();
notifySetupModeDataChange();
};
export const toggleSetupMode = inSetupMode => {
checkAngularState();
const globalState = angularState.injector.get('globalState');
angularState.scope.$evalAsync(async () => {
setupModeState.enabled = inSetupMode;
globalState.inSetupMode = inSetupMode;
globalState.save();
setSetupModeMenuItem(); // eslint-disable-line no-use-before-define
notifySetupModeDataChange();
if (inSetupMode) {
await updateSetupModeData();
}
});
};
const setSetupModeMenuItem = () => {
// Disabling this for this initial release. This will be added back in
// in a subsequent PR
// checkAngularState();
// const globalState = angularState.injector.get('globalState');
// const navItems = globalState.inSetupMode
// ? [
// {
// key: 'exit',
// label: 'Exit Setup Mode',
// description: 'Exit setup mode',
// run: () => toggleSetupMode(false),
// testId: 'exitSetupMode'
// },
// {
// key: 'refresh',
// label: 'Refresh Setup Data',
// description: 'Refresh data used for setup mode',
// run: () => updateSetupModeData(),
// testId: 'refreshSetupModeData'
// }
// ]
// : [{
// key: 'enter',
// label: 'Enter Setup Mode',
// description: 'Enter setup mode',
// run: () => toggleSetupMode(true),
// testId: 'enterSetupMode'
// }];
// angularState.scope.topNavMenu = [...navItems];
};
export const initSetupModeState = (callback) => {
checkAngularState();
setSetupModeMenuItem();
callback && setupModeState.callbacks.push(callback);
const globalState = angularState.injector.get('globalState');
if (globalState.inSetupMode) {
toggleSetupMode(true);
}
};

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 { i18n } from '@kbn/i18n';
import { find } from 'lodash';
import uiRoutes from 'ui/routes';
@ -13,6 +13,7 @@ import { routeInitProvider } from 'plugins/monitoring/lib/route_init';
import { MonitoringViewBaseEuiTableController } from '../../';
import { ElasticsearchNodes } from '../../../components';
import { I18nContext } from 'ui/i18n';
import { SetupModeRenderer } from '../../../components/renderers';
uiRoutes.when('/elasticsearch/nodes', {
template,
@ -54,13 +55,24 @@ uiRoutes.when('/elasticsearch/nodes', {
this.renderReact = ({ clusterStatus, nodes }) => {
super.renderReact(
<I18nContext>
<ElasticsearchNodes
clusterStatus={clusterStatus}
nodes={nodes}
showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch}
sorting={this.sorting}
pagination={this.pagination}
onTableChange={this.onTableChange}
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName="elasticsearch"
render={({ setupMode, flyoutComponent }) => (
<Fragment>
{flyoutComponent}
<ElasticsearchNodes
clusterStatus={clusterStatus}
setupMode={setupMode}
nodes={nodes}
showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch}
sorting={this.sorting}
pagination={this.pagination}
onTableChange={this.onTableChange}
/>
</Fragment>
)}
/>
</I18nContext>
);

View file

@ -4,111 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { capitalize } from 'lodash';
import { I18nContext } from 'ui/i18n';
import React, { Fragment } from 'react';
import uiRoutes from'ui/routes';
import { routeInitProvider } from 'plugins/monitoring/lib/route_init';
import { MonitoringViewBaseEuiTableController } from '../../';
import { getPageData } from './get_page_data';
import template from './index.html';
import { EuiPage, EuiPageBody, EuiPageContent, EuiPanel, EuiSpacer, EuiLink } from '@elastic/eui';
import { ClusterStatus } from '../../../components/kibana/cluster_status';
import { EuiMonitoringTable } from '../../../components/table';
import { KibanaStatusIcon } from '../../../components/kibana/status_icon';
import { formatMetric, formatNumber } from '../../../lib/format_number';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
const getColumns = (kbnUrl, scope) => ([
{
name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', {
defaultMessage: 'Name'
}),
field: 'name',
render: (name, kibana) => (
<EuiLink
onClick={() => {
scope.$evalAsync(() => {
kbnUrl.changePath(`/kibana/instances/${kibana.kibana.uuid}`);
});
}}
data-test-subj={`kibanaLink-${name}`}
>
{ name }
</EuiLink>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', {
defaultMessage: 'Status'
}),
field: 'status',
render: (status, kibana) => (
<div
title={`Instance status: ${status}`}
className="monTableCell__status"
>
<KibanaStatusIcon status={status} availability={kibana.availability} />&nbsp;
{ !kibana.availability ? (
<FormattedMessage
id="xpack.monitoring.kibana.listing.instanceStatus.offlineLabel"
defaultMessage="Offline"
/>
) : capitalize(status) }
</div>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.loadAverageColumnTitle', {
defaultMessage: 'Load Average'
}),
field: 'os.load.1m',
render: value => (
<span>
{formatMetric(value, '0.00')}
</span>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.memorySizeColumnTitle', {
defaultMessage: 'Memory Size'
}),
field: 'process.memory.resident_set_size_in_bytes',
render: value => (
<span>
{formatNumber(value, 'byte')}
</span>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.requestsColumnTitle', {
defaultMessage: 'Requests'
}),
field: 'requests.total',
render: value => (
<span>
{formatNumber(value, 'int_commas')}
</span>
)
},
{
name: i18n.translate('xpack.monitoring.kibana.listing.responseTimeColumnTitle', {
defaultMessage: 'Response Times'
}),
field: 'response_times.average',
render: (value, kibana) => (
<div>
<div className="monTableCell__splitNumber">
{ value && (formatNumber(value, 'int_commas') + ' ms avg') }
</div>
<div className="monTableCell__splitNumber">
{ formatNumber(kibana.response_times.max, 'int_commas') } ms max
</div>
</div>
)
}
]);
import { KibanaInstances } from 'plugins/monitoring/components/kibana/instances';
import { SetupModeRenderer } from '../../../components/renderers';
import { I18nContext } from 'ui/i18n';
uiRoutes.when('/kibana/instances', {
template,
@ -134,51 +38,41 @@ uiRoutes.when('/kibana/instances', {
const kbnUrl = $injector.get('kbnUrl');
const renderReact = () => {
this.renderReact(
<I18nContext>
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName="kibana"
render={({ setupMode, flyoutComponent }) => (
<Fragment>
{flyoutComponent}
<KibanaInstances
instances={this.data.kibanas}
setupMode={setupMode}
sorting={this.sorting}
pagination={this.pagination}
onTableChange={this.onTableChange}
clusterStatus={this.data.clusterStatus}
angular={{
$scope,
kbnUrl,
}}
/>
</Fragment>
)}
/>
</I18nContext>
);
};
$scope.$watch(() => this.data, data => {
if (!data || !data.kibanas) {
if (!data) {
return;
}
const dataFlattened = data.kibanas.map(item => ({
...item,
name: item.kibana.name,
status: item.kibana.status,
}));
this.renderReact(
<I18nContext>
<EuiPage>
<EuiPageBody>
<EuiPanel>
<ClusterStatus stats={$scope.pageData.clusterStatus} />
</EuiPanel>
<EuiSpacer size="m" />
<EuiPageContent>
<EuiMonitoringTable
className="kibanaInstancesTable"
rows={dataFlattened}
columns={getColumns(kbnUrl, $scope)}
sorting={this.sorting}
pagination={this.pagination}
search={{
box: {
incremental: true,
placeholder: i18n.translate('xpack.monitoring.kibana.listing.filterInstancesPlaceholder', {
defaultMessage: 'Filter Instances…'
})
},
}}
onTableChange={this.onTableChange}
executeQueryOptions={{
defaultFields: ['name']
}}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</I18nContext>
);
renderReact();
});
}
}

View file

@ -5,14 +5,14 @@
*/
import { get, uniq } from 'lodash';
import { METRICBEAT_INDEX_NAME_UNIQUE_TOKEN } from '../../../../common/constants';
import { METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, ELASTICSEARCH_CUSTOM_ID } from '../../../../common/constants';
import { KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID, LOGSTASH_SYSTEM_ID } from '../../../../../telemetry/common/constants';
const NUMBER_OF_SECONDS_AGO_TO_LOOK = 30;
const APM_CUSTOM_ID = 'apm';
const ELASTICSEARCH_CUSTOM_ID = 'elasticsearch';
const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid) => {
const start = get(req.payload, 'timeRange.min', 'now-30s');
const start = get(req.payload, 'timeRange.min', `now-${NUMBER_OF_SECONDS_AGO_TO_LOOK}s`);
const end = get(req.payload, 'timeRange.max', 'now');
const filters = [
@ -247,6 +247,9 @@ function shouldSkipBucket(product, bucket) {
* @param {*} clusterUuid Optional and will be used to filter down the query if used
*/
export const getCollectionStatus = async (req, indexPatterns, clusterUuid) => {
const config = req.server.config();
const kibanaUuid = config.get('server.uuid');
const PRODUCTS = [
{ name: KIBANA_SYSTEM_ID },
{ name: BEATS_SYSTEM_ID },
@ -273,8 +276,9 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid) => {
const productStatus = {
totalUniqueInstanceCount: 0,
totalUniqueFullyMigratedCount: 0,
totalUniquePartiallyMigratedCount: 0,
detected: null,
byUuid: null,
byUuid: {},
};
const fullyMigratedUuidsMap = {};
@ -291,7 +295,6 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid) => {
const singleIndexBucket = indexBuckets[0];
const isFullyMigrated = singleIndexBucket.key.includes(METRICBEAT_INDEX_NAME_UNIQUE_TOKEN);
const map = isFullyMigrated ? fullyMigratedUuidsMap : internalCollectorsUuidsMap;
const uuidBuckets = get(singleIndexBucket, `${uuidBucketName}.buckets`, []);
for (const bucket of uuidBuckets) {
@ -301,9 +304,13 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid) => {
const { key, by_timestamp: byTimestamp } = bucket;
if (!map[key]) {
map[key] = { lastTimestamp: get(byTimestamp, 'value') };
if (product.name === KIBANA_SYSTEM_ID && key === kibanaUuid) {
map[key].isPrimary = true;
}
}
}
productStatus.totalUniqueInstanceCount = Object.keys(map).length;
productStatus.totalUniquePartiallyMigratedCount = Object.keys(partiallyMigratedUuidsMap).length;
productStatus.totalUniqueFullyMigratedCount = Object.keys(fullyMigratedUuidsMap).length;
productStatus.byUuid = {
...Object.keys(internalCollectorsUuidsMap).reduce((accum, uuid) => ({
@ -337,11 +344,14 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid) => {
const { key, by_timestamp: byTimestamp } = bucket;
if (!map[key]) {
if (otherMap[key]) {
partiallyMigratedUuidsMap[key] = true;
partiallyMigratedUuidsMap[key] = otherMap[key] || {};
delete otherMap[key];
}
else {
map[key] = true;
map[key] = {};
if (product.name === KIBANA_SYSTEM_ID && key === kibanaUuid) {
map[key].isPrimary = true;
}
}
}
if (!isFullyMigrated) {
@ -355,19 +365,30 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid) => {
...Object.keys(fullyMigratedUuidsMap),
...Object.keys(partiallyMigratedUuidsMap)
]).length;
productStatus.totalUniquePartiallyMigratedCount = Object.keys(partiallyMigratedUuidsMap).length;
productStatus.totalUniqueFullyMigratedCount = Object.keys(fullyMigratedUuidsMap).length;
productStatus.byUuid = {
...Object.keys(internalCollectorsUuidsMap).reduce((accum, uuid) => ({
...accum,
[uuid]: { isInternalCollector: true }
[uuid]: {
isInternalCollector: true,
...internalCollectorsUuidsMap[uuid]
}
}), {}),
...Object.keys(partiallyMigratedUuidsMap).reduce((accum, uuid) => ({
...accum,
[uuid]: { isPartiallyMigrated: true, lastInternallyCollectedTimestamp: internalTimestamps[0] }
[uuid]: {
isPartiallyMigrated: true,
lastInternallyCollectedTimestamp: internalTimestamps[0],
...partiallyMigratedUuidsMap[uuid]
}
}), {}),
...Object.keys(fullyMigratedUuidsMap).reduce((accum, uuid) => ({
...accum,
[uuid]: { isFullyMigrated: true }
[uuid]: {
isFullyMigrated: true,
...fullyMigratedUuidsMap[uuid]
}
}), {}),
};
}
@ -378,5 +399,9 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid) => {
};
}, {});
status._meta = {
secondsAgo: NUMBER_OF_SECONDS_AGO_TO_LOOK,
};
return status;
};

View file

@ -1,42 +1,50 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
},
"beats": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": true
},
"byUuid": null
"byUuid": {}
},
"elasticsearch": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
}
}

View file

@ -1,11 +1,16 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 1,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"5b2de169-2785-441b-ae8c-186a1936b17d": {
"isFullyMigrated": true,
"isPrimary": true,
"lastTimestamp": 1554841069921
}
}
@ -13,30 +18,34 @@
"beats": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": true
},
"byUuid": null
"byUuid": {}
},
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"elasticsearch": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 1,
"detected": null,
"byUuid": {
"agI8JhXhShasvuDgq0VxRg": {

View file

@ -1,42 +1,50 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
},
"beats": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": true
},
"byUuid": null
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"elasticsearch": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
}
}

View file

@ -1,42 +1,50 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
},
"beats": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": true
},
"byUuid": null
"byUuid": {}
},
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"elasticsearch": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
}
}

View file

@ -1,42 +1,50 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
},
"beats": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": true
},
"byUuid": null
"byUuid": {}
},
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"mightExist": false
},
"byUuid": null
"byUuid": {}
},
"elasticsearch": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": {
"doesExist": true
},
"byUuid": null
"byUuid": {}
}
}

View file

@ -1,11 +1,16 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 1,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"5b2de169-2785-441b-ae8c-186a1936b17d": {
"isFullyMigrated": true,
"isPrimary": true,
"lastTimestamp": 1554821587077
}
}
@ -13,6 +18,7 @@
"beats": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
@ -24,12 +30,14 @@
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
@ -41,6 +49,7 @@
"elasticsearch": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 1,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"agI8JhXhShasvuDgq0VxRg": {

View file

@ -1,11 +1,16 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 1,
"detected": null,
"byUuid": {
"5b2de169-2785-441b-ae8c-186a1936b17d": {
"isPartiallyMigrated": true,
"isPrimary": true,
"lastInternallyCollectedTimestamp": 1554821412725
}
}
@ -13,6 +18,7 @@
"beats": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
@ -24,12 +30,14 @@
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
@ -41,6 +49,7 @@
"elasticsearch": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 1,
"detected": null,
"byUuid": {
"agI8JhXhShasvuDgq0VxRg": {

View file

@ -1,11 +1,16 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 1,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"5b2de169-2785-441b-ae8c-186a1936b17d": {
"isFullyMigrated": true,
"isPrimary": true,
"lastTimestamp": 1554821537079
}
}
@ -13,6 +18,7 @@
"beats": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
@ -24,12 +30,14 @@
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
@ -41,6 +49,7 @@
"elasticsearch": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 1,
"detected": null,
"byUuid": {
"agI8JhXhShasvuDgq0VxRg": {

View file

@ -1,11 +1,16 @@
{
"_meta": {
"secondsAgo": 30
},
"kibana": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 1,
"detected": null,
"byUuid": {
"5b2de169-2785-441b-ae8c-186a1936b17d": {
"isPartiallyMigrated": true,
"isPrimary": true,
"lastInternallyCollectedTimestamp": 1554821352739
}
}
@ -13,6 +18,7 @@
"beats": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
@ -24,12 +30,14 @@
"apm": {
"totalUniqueInstanceCount": 0,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {}
},
"logstash": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
@ -41,6 +49,7 @@
"elasticsearch": {
"totalUniqueInstanceCount": 1,
"totalUniqueFullyMigratedCount": 0,
"totalUniquePartiallyMigratedCount": 0,
"detected": null,
"byUuid": {
"agI8JhXhShasvuDgq0VxRg": {