[Monitoring] Metricbeat migration for logstash, beats and apm (#40442) (#42455)

* Support for logstash

* Beats support

* Fix cherry-pick api issue

* Support for logstash

* Updates for beats and logstash

* APM migration working

* Tweaks for beats migration

* Update copy for setup new button

* If on cloud, disable setup mode

* Handle new beat flow better

* Better phrasing for APM

* Add beat type to disable step

* Fix i18n issue

* Fix jest tests

* Fix api tests

* PR feedback

* Update copy

* Remove unnecessary code

* Undo changes that are now in a separate PR

* Disable more links

* Fix overview link for logstash

* PR feedback

* Fix tests

* PR feedback

* PR feedback

* Capitalize Beat per PR feedback
This commit is contained in:
Chris Roberson 2019-08-01 13:40:18 -04:00 committed by GitHub
parent ba516a41f5
commit 1597b30132
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 2426 additions and 221 deletions

View file

@ -167,6 +167,7 @@ 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';
export const APM_CUSTOM_ID = 'apm';
/**
* The id of the infra source owned by the monitoring plugin.
*/

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { Fragment } from 'react';
import moment from 'moment';
import { uniq } from 'lodash';
import { uniq, get } from 'lodash';
import { EuiMonitoringTable } from '../../table';
import { EuiLink, EuiPage, EuiPageBody, EuiPageContent, EuiSpacer } from '@elastic/eui';
import { EuiLink, EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { Status } from './status';
import { formatMetric } from '../../../lib/format_number';
import { formatTimestampToDuration } from '../../../../common';
@ -83,14 +83,37 @@ const columns = [
},
];
export function ApmServerInstances({ apms }) {
export function ApmServerInstances({ apms, setupMode }) {
const {
pagination,
sorting,
onTableChange,
data
data,
} = apms;
let detectedInstanceMessage = null;
if (setupMode.enabled && setupMode.data && get(setupMode.data, 'detected.mightExist')) {
detectedInstanceMessage = (
<Fragment>
<EuiCallOut
title={i18n.translate('xpack.monitoring.apm.instances.metricbeatMigration.detectedInstanceTitle', {
defaultMessage: 'APM server detected',
})}
color="warning"
iconType="help"
>
<p>
{i18n.translate('xpack.monitoring.apm.instances.metricbeatMigration.detectedInstanceDescription', {
defaultMessage: `Based on your indices, we think you might have an APM server. Click the 'Setup monitoring'
button below to start monitoring this APM server.`
})}
</p>
</EuiCallOut>
<EuiSpacer size="m"/>
</Fragment>
);
}
const versions = uniq(data.apms.map(item => item.version)).map(version => {
return { value: version };
});
@ -101,12 +124,19 @@ export function ApmServerInstances({ apms }) {
<EuiPageContent>
<Status stats={data.stats} />
<EuiSpacer size="m"/>
{detectedInstanceMessage}
<EuiMonitoringTable
className="apmInstancesTable"
rows={data.apms}
columns={columns}
sorting={sorting}
pagination={pagination}
setupMode={setupMode}
uuidField="uuid"
nameField="name"
setupNewButtonLabel={i18n.translate('xpack.monitoring.apm.metricbeatMigration.setupNewButtonLabel', {
defaultMessage: 'Setup monitoring for new APM server'
})}
search={{
box: {
incremental: true,

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PureComponent } from 'react';
import { uniq } from 'lodash';
import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui';
import React, { PureComponent, Fragment } from 'react';
import { uniq, get } from 'lodash';
import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink, EuiCallOut } from '@elastic/eui';
import { Stats } from 'plugins/monitoring/components/beats';
import { formatMetric } from 'plugins/monitoring/lib/format_number';
import { EuiMonitoringTable } from 'plugins/monitoring/components/table';
@ -74,9 +74,32 @@ export class Listing extends PureComponent {
data,
sorting,
pagination,
onTableChange
onTableChange,
setupMode
} = this.props;
let detectedInstanceMessage = null;
if (setupMode.enabled && setupMode.data && get(setupMode.data, 'detected.mightExist')) {
detectedInstanceMessage = (
<Fragment>
<EuiCallOut
title={i18n.translate('xpack.monitoring.beats.instances.metricbeatMigration.detectedInstanceTitle', {
defaultMessage: 'Beats instance detected',
})}
color="warning"
iconType="help"
>
<p>
{i18n.translate('xpack.monitoring.beats.instances.metricbeatMigration.detectedInstanceDescription', {
defaultMessage: `Based on your indices, we think you might have a beats instance. Click the 'Setup monitoring'
button below to start monitoring this instance.`
})}
</p>
</EuiCallOut>
<EuiSpacer size="m"/>
</Fragment>
);
}
const types = uniq(data.map(item => item.type)).map(type => {
return { value: type };
@ -92,9 +115,16 @@ export class Listing extends PureComponent {
<EuiPageContent>
<Stats stats={stats} />
<EuiSpacer size="m"/>
{detectedInstanceMessage}
<EuiMonitoringTable
className="beatsTable"
rows={data}
setupMode={setupMode}
uuidField="uuid"
nameField="name"
setupNewButtonLabel={i18n.translate('xpack.monitoring.beats.metricbeatMigration.setupNewButtonLabel', {
defaultMessage: 'Setup monitoring for new Beats instance'
})}
columns={this.getColumns()}
sorting={sorting}
pagination={pagination}

View file

@ -8,10 +8,9 @@ import React from 'react';
import moment from 'moment';
import { get } from 'lodash';
import { formatMetric } from 'plugins/monitoring/lib/format_number';
import { ClusterItemContainer, BytesPercentageUsage } from './helpers';
import { ClusterItemContainer, BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink } from './helpers';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGrid,
EuiFlexItem,
@ -22,18 +21,55 @@ import {
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiHorizontalRule,
EuiFlexGroup,
EuiToolTip,
EuiBadge
} from '@elastic/eui';
import { formatTimestampToDuration } from '../../../../common';
import { CALCULATE_DURATION_SINCE } from '../../../../common/constants';
export function ApmPanel(props) {
if (!get(props, 'apms.total', 0) > 0) {
const { setupMode } = props;
const apmsTotal = get(props, 'apms.total') || 0;
// Do not show if we are not in setup mode
if (apmsTotal === 0 && !setupMode.enabled) {
return null;
}
const goToApm = () => props.changeUrl('apm');
const goToInstances = () => props.changeUrl('apm/instances');
const setupModeApmData = get(setupMode.data, 'apm');
let setupModeInstancesData = null;
if (setupMode.enabled && setupMode.data) {
const migratedNodesCount = Object.values(setupModeApmData.byUuid).filter(node => node.isFullyMigrated).length;
let totalNodesCount = Object.values(setupModeApmData.byUuid).length;
if (totalNodesCount === 0 && get(setupMode.data, 'apm.detected.mightExist', false)) {
totalNodesCount = 1;
}
const badgeColor = migratedNodesCount === totalNodesCount
? 'secondary'
: 'danger';
setupModeInstancesData = (
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={i18n.translate('xpack.monitoring.cluster.overview.apmPanel.setupModeNodesTooltip', {
defaultMessage: `These numbers indicate how many detected monitored APM servers versus how many ` +
`detected total APM servers. If there are more detected APM servers than monitored APM servers, click the Nodes ` +
`link and you will be guided in how to setup monitoring for the missing node.`
})}
>
<EuiBadge color={badgeColor}>
{migratedNodesCount}/{totalNodesCount}
</EuiBadge>
</EuiToolTip>
</EuiFlexItem>
);
}
return (
<ClusterItemContainer
{...props}
@ -47,7 +83,9 @@ export function ApmPanel(props) {
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeApmData}
onClick={goToApm}
aria-label={i18n.translate('xpack.monitoring.cluster.overview.apmPanel.overviewLinkAriaLabel', {
defaultMessage: 'APM Overview'
@ -58,7 +96,7 @@ export function ApmPanel(props) {
id="xpack.monitoring.cluster.overview.apmPanel.overviewLinkLabel"
defaultMessage="Overview"
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />
@ -90,27 +128,32 @@ export function ApmPanel(props) {
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
onClick={goToInstances}
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel',
{
defaultMessage: 'Apm Instances: {apmsTotal}',
values: { apmsTotal: props.apms.total }
}
)}
data-test-subj="apmListing"
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.apmPanel.serversTotalLinkLabel"
defaultMessage="APM Servers: {apmsTotal}"
values={{ apmsTotal: (<span data-test-subj="apmsTotal">{props.apms.total}</span>) }}
/>
</EuiLink>
</h3>
</EuiTitle>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
<EuiLink
onClick={goToInstances}
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel',
{
defaultMessage: 'APM Instances: {apmsTotal}',
values: { apmsTotal }
}
)}
data-test-subj="apmListing"
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.apmPanel.serversTotalLinkLabel"
defaultMessage="APM Servers: {apmsTotal}"
values={{ apmsTotal: (<span data-test-subj="apmsTotal">{apmsTotal}</span>) }}
/>
</EuiLink>
</h3>
</EuiTitle>
</EuiFlexItem>
{setupModeInstancesData}
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiDescriptionList type="column">
<EuiDescriptionListTitle>

View file

@ -17,19 +17,56 @@ import {
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiHorizontalRule,
EuiFlexGroup,
EuiToolTip,
EuiBadge
} from '@elastic/eui';
import { ClusterItemContainer } from './helpers';
import { ClusterItemContainer, DisabledIfNoDataAndInSetupModeLink } from './helpers';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
export function BeatsPanel(props) {
if (!get(props, 'beats.total', 0) > 0) {
const { setupMode } = props;
const beatsTotal = get(props, 'beats.total') || 0;
// Do not show if we are not in setup mode
if (beatsTotal === 0 && !setupMode.enabled) {
return null;
}
const goToBeats = () => props.changeUrl('beats');
const goToInstances = () => props.changeUrl('beats/beats');
const setupModeBeatsData = get(setupMode.data, 'beats');
let setupModeInstancesData = null;
if (setupMode.enabled && setupMode.data) {
const migratedNodesCount = Object.values(setupModeBeatsData.byUuid).filter(node => node.isFullyMigrated).length;
let totalNodesCount = Object.values(setupModeBeatsData.byUuid).length;
if (totalNodesCount === 0 && get(setupMode.data, 'beats.detected.mightExist', false)) {
totalNodesCount = 1;
}
const badgeColor = migratedNodesCount === totalNodesCount
? 'secondary'
: 'danger';
setupModeInstancesData = (
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={i18n.translate('xpack.monitoring.cluster.overview.beatsPanel.setupModeNodesTooltip', {
defaultMessage: `These numbers indicate how many detected monitored Beats versus how many ` +
`detected total Beats. If there are more detected Beats than monitored Beats, click the Nodes ` +
`link and you will be guided in how to setup monitoring for the missing node.`
})}
>
<EuiBadge color={badgeColor}>
{migratedNodesCount}/{totalNodesCount}
</EuiBadge>
</EuiToolTip>
</EuiFlexItem>
);
}
const beatTypes = props.beats.types.map((beat, index) => {
return [
<EuiDescriptionListTitle
@ -60,7 +97,9 @@ export function BeatsPanel(props) {
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeBeatsData}
onClick={goToBeats}
aria-label={i18n.translate('xpack.monitoring.cluster.overview.beatsPanel.overviewLinkAriaLabel', {
defaultMessage: 'Beats Overview'
@ -71,7 +110,7 @@ export function BeatsPanel(props) {
id="xpack.monitoring.cluster.overview.beatsPanel.overviewLinkLabel"
defaultMessage="Overview"
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />
@ -99,27 +138,32 @@ export function BeatsPanel(props) {
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
onClick={goToInstances}
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.beatsPanel.instancesTotalLinkAriaLabel',
{
defaultMessage: 'Beats Instances: {beatsTotal}',
values: { beatsTotal: props.beats.total }
}
)}
data-test-subj="beatsListing"
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.beatsPanel.beatsTotalLinkLabel"
defaultMessage="Beats: {beatsTotal}"
values={{ beatsTotal: (<span data-test-subj="beatsTotal">{props.beats.total}</span>) }}
/>
</EuiLink>
</h3>
</EuiTitle>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
<EuiLink
onClick={goToInstances}
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.beatsPanel.instancesTotalLinkAriaLabel',
{
defaultMessage: 'Beats Instances: {beatsTotal}',
values: { beatsTotal }
}
)}
data-test-subj="beatsListing"
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.beatsPanel.beatsTotalLinkLabel"
defaultMessage="Beats: {beatsTotal}"
values={{ beatsTotal: (<span data-test-subj="beatsTotal">{beatsTotal}</span>) }}
/>
</EuiLink>
</h3>
</EuiTitle>
</EuiFlexItem>
{setupModeInstancesData}
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiDescriptionList type="column">
{beatTypes}

View file

@ -7,7 +7,13 @@
import React, { Fragment } from 'react';
import { get, capitalize } from 'lodash';
import { formatNumber } from 'plugins/monitoring/lib/format_number';
import { ClusterItemContainer, HealthStatusIndicator, BytesUsage, BytesPercentageUsage } from './helpers';
import {
ClusterItemContainer,
HealthStatusIndicator,
BytesUsage,
BytesPercentageUsage,
DisabledIfNoDataAndInSetupModeLink
} from './helpers';
import {
EuiFlexGrid,
EuiFlexItem,
@ -151,36 +157,13 @@ export function ElasticsearchPanel(props) {
<HealthStatusIndicator status={clusterStats.status} />
);
const showMlJobs = () => {
// if license doesn't support ML, then `ml === null`
if (props.ml) {
const gotoURL = '#/elasticsearch/ml_jobs';
return (
<>
<EuiDescriptionListTitle>
<EuiLink href={gotoURL}>
<FormattedMessage
id="xpack.monitoring.cluster.overview.esPanel.jobsLabel"
defaultMessage="Jobs"
/>
</EuiLink>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription data-test-subj="esMlJobs">
<EuiLink href={gotoURL}>{props.ml.jobs}</EuiLink>
</EuiDescriptionListDescription>
</>
);
}
return null;
};
const licenseText = <LicenseText license={props.license} showLicenseExpiration={props.showLicenseExpiration} />;
const setupModeElasticsearchData = get(setupMode.data, 'elasticsearch');
let setupModeNodesData = null;
if (setupMode.enabled && setupMode.data) {
const elasticsearchData = get(setupMode.data, 'elasticsearch.byUuid');
const migratedNodesCount = Object.values(elasticsearchData).filter(node => node.isFullyMigrated).length;
const totalNodesCount = Object.values(elasticsearchData).length;
const migratedNodesCount = Object.values(setupModeElasticsearchData.byUuid).filter(node => node.isFullyMigrated).length;
const totalNodesCount = Object.values(setupModeElasticsearchData.byUuid).length;
const badgeColor = migratedNodesCount === totalNodesCount
? 'secondary'
@ -204,6 +187,39 @@ export function ElasticsearchPanel(props) {
);
}
const showMlJobs = () => {
// if license doesn't support ML, then `ml === null`
if (props.ml) {
const gotoURL = '#/elasticsearch/ml_jobs';
return (
<>
<EuiDescriptionListTitle>
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeElasticsearchData}
href={gotoURL}
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.esPanel.jobsLabel"
defaultMessage="Jobs"
/>
</DisabledIfNoDataAndInSetupModeLink>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription data-test-subj="esMlJobs">
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeElasticsearchData}
href={gotoURL}
>
{props.ml.jobs}
</DisabledIfNoDataAndInSetupModeLink>
</EuiDescriptionListDescription>
</>
);
}
return null;
};
return (
<ClusterItemContainer
{...props}
@ -218,7 +234,9 @@ export function ElasticsearchPanel(props) {
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeElasticsearchData}
onClick={goToElasticsearch}
aria-label={i18n.translate('xpack.monitoring.cluster.overview.esPanel.overviewLinkAriaLabel', {
defaultMessage: 'Elasticsearch Overview'
@ -229,7 +247,7 @@ export function ElasticsearchPanel(props) {
id="xpack.monitoring.cluster.overview.esPanel.overviewLinkLabel"
defaultMessage="Overview"
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />
@ -318,7 +336,9 @@ export function ElasticsearchPanel(props) {
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeElasticsearchData}
onClick={goToIndices}
data-test-subj="esNumberOfIndices"
aria-label={i18n.translate('xpack.monitoring.cluster.overview.esPanel.indicesCountLinkAriaLabel', {
@ -331,7 +351,7 @@ export function ElasticsearchPanel(props) {
defaultMessage="Indices: {indicesCount}"
values={{ indicesCount: formatNumber(get(indices, 'count'), 'int_commas') }}
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />
@ -383,7 +403,9 @@ export function ElasticsearchPanel(props) {
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeElasticsearchData}
onClick={goToElasticsearch}
aria-label={i18n.translate('xpack.monitoring.cluster.overview.esPanel.logsLinkAriaLabel', {
defaultMessage: 'Elasticsearch Logs'
@ -394,7 +416,7 @@ export function ElasticsearchPanel(props) {
id="xpack.monitoring.cluster.overview.esPanel.logsLinkLabel"
defaultMessage="Logs"
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />

View file

@ -5,8 +5,8 @@
*/
import React from 'react';
import { get } from 'lodash';
import { formatBytesUsage, formatPercentageUsage } from 'plugins/monitoring/lib/format_number';
import {
EuiSpacer,
EuiFlexItem,
@ -15,6 +15,7 @@ import {
EuiIcon,
EuiHealth,
EuiText,
EuiLink
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
@ -123,5 +124,15 @@ export function BytesPercentageUsage({ usedBytes, maxBytes }) {
);
}
return null;
return <EuiText>0</EuiText>;
}
export function DisabledIfNoDataAndInSetupModeLink({ setupModeEnabled, setupModeData, children, ...props }) {
if (setupModeEnabled && get(setupModeData, 'totalUniqueInstanceCount', 0) === 0) {
return children;
}
return (
<EuiLink {...props}>{children}</EuiLink>
);
}

View file

@ -45,11 +45,11 @@ export function Overview(props) {
: null
}
<LogstashPanel {...props.cluster.logstash} changeUrl={props.changeUrl} />
<LogstashPanel {...props.cluster.logstash} setupMode={props.setupMode} changeUrl={props.changeUrl} />
<BeatsPanel {...props.cluster.beats} changeUrl={props.changeUrl} />
<BeatsPanel {...props.cluster.beats} setupMode={props.setupMode} changeUrl={props.changeUrl} />
<ApmPanel {...props.cluster.apm} changeUrl={props.changeUrl} />
<ApmPanel {...props.cluster.apm} setupMode={props.setupMode} changeUrl={props.changeUrl} />
</EuiPageBody>
</EuiPage>
);

View file

@ -6,7 +6,7 @@
import React from 'react';
import { formatNumber } from 'plugins/monitoring/lib/format_number';
import { ClusterItemContainer, HealthStatusIndicator, BytesPercentageUsage } from './helpers';
import { ClusterItemContainer, HealthStatusIndicator, BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink } from './helpers';
import { get } from 'lodash';
import {
EuiFlexGrid,
@ -39,11 +39,11 @@ export function KibanaPanel(props) {
const goToKibana = () => props.changeUrl('kibana');
const goToInstances = () => props.changeUrl('kibana/instances');
const setupModeKibanaData = get(setupMode.data, 'kibana');
let setupModeInstancesData = null;
if (setupMode.enabled && setupMode.data) {
const kibanaData = get(setupMode.data, 'kibana.byUuid');
const migratedNodesCount = Object.values(kibanaData).filter(node => node.isFullyMigrated).length;
const totalNodesCount = Object.values(kibanaData).length;
const migratedNodesCount = Object.values(setupModeKibanaData.byUuid).filter(node => node.isFullyMigrated).length;
const totalNodesCount = Object.values(setupModeKibanaData.byUuid).length;
const badgeColor = migratedNodesCount === totalNodesCount
? 'secondary'
@ -81,7 +81,9 @@ export function KibanaPanel(props) {
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeKibanaData}
onClick={goToKibana}
aria-label={i18n.translate('xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkAriaLabel', {
defaultMessage: 'Kibana Overview'
@ -92,7 +94,7 @@ export function KibanaPanel(props) {
id="xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkLabel"
defaultMessage="Overview"
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />

View file

@ -6,7 +6,7 @@
import React from 'react';
import { formatNumber } from 'plugins/monitoring/lib/format_number';
import { ClusterItemContainer, BytesPercentageUsage } from './helpers';
import { ClusterItemContainer, BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink } from './helpers';
import { LOGSTASH } from '../../../../common/constants';
import {
@ -21,12 +21,20 @@ import {
EuiDescriptionListDescription,
EuiHorizontalRule,
EuiIconTip,
EuiToolTip,
EuiBadge
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
export function LogstashPanel(props) {
if (!props.node_count) {
const { setupMode } = props;
const nodesCount = props.node_count || 0;
const queueTypes = props.queue_types || {};
// Do not show if we are not in setup mode
if (!nodesCount && !setupMode.enabled) {
return null;
}
@ -34,6 +42,37 @@ export function LogstashPanel(props) {
const goToNodes = () => props.changeUrl('logstash/nodes');
const goToPipelines = () => props.changeUrl('logstash/pipelines');
const setupModeLogstashData = get(setupMode.data, 'logstash');
let setupModeInstancesData = null;
if (setupMode.enabled && setupMode.data) {
const migratedNodesCount = Object.values(setupModeLogstashData.byUuid).filter(node => node.isFullyMigrated).length;
let totalNodesCount = Object.values(setupModeLogstashData.byUuid).length;
if (totalNodesCount === 0 && get(setupMode.data, 'logstash.detected.mightExist', false)) {
totalNodesCount = 1;
}
const badgeColor = migratedNodesCount === totalNodesCount
? 'secondary'
: 'danger';
setupModeInstancesData = (
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={i18n.translate('xpack.monitoring.cluster.overview.logstashPanel.setupModeNodesTooltip', {
defaultMessage: `These numbers indicate how many detected monitored nodes versus how many ` +
`detected total nodes. If there are more detected nodes than monitored nodes, click the Nodes ` +
`link and you will be guided in how to setup monitoring for the missing node.`
})}
>
<EuiBadge color={badgeColor}>
{formatNumber(migratedNodesCount, 'int_commas')}/{formatNumber(totalNodesCount, 'int_commas')}
</EuiBadge>
</EuiToolTip>
</EuiFlexItem>
);
}
return (
<ClusterItemContainer
{...props}
@ -47,7 +86,9 @@ export function LogstashPanel(props) {
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeLogstashData}
onClick={goToLogstash}
aria-label={i18n.translate('xpack.monitoring.cluster.overview.logstashPanel.overviewLinkAriaLabel', {
defaultMessage: 'Logstash Overview'
@ -57,7 +98,7 @@ export function LogstashPanel(props) {
id="xpack.monitoring.cluster.overview.logstashPanel.overviewLinkLabel"
defaultMessage="Overview"
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />
@ -86,27 +127,32 @@ export function LogstashPanel(props) {
<EuiFlexItem>
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
onClick={goToNodes}
data-test-subj="lsNodes"
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.logstashPanel.nodesCountLinkAriaLabel',
{
defaultMessage: 'Logstash Nodes: {nodesCount}',
values: { nodesCount: props.node_count }
}
)}
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.logstashPanel.nodesCountLinkLabel"
defaultMessage="Nodes: {nodesCount}"
values={{ nodesCount: (<span data-test-subj="number_of_logstash_instances">{ props.node_count }</span>) }}
/>
</EuiLink>
</h3>
</EuiTitle>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
<EuiLink
onClick={goToNodes}
data-test-subj="lsNodes"
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.logstashPanel.nodesCountLinkAriaLabel',
{
defaultMessage: 'Logstash Nodes: {nodesCount}',
values: { nodesCount }
}
)}
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.logstashPanel.nodesCountLinkLabel"
defaultMessage="Nodes: {nodesCount}"
values={{ nodesCount: (<span data-test-subj="number_of_logstash_instances">{ nodesCount }</span>) }}
/>
</EuiLink>
</h3>
</EuiTitle>
</EuiFlexItem>
{setupModeInstancesData}
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiDescriptionList type="column">
<EuiDescriptionListTitle>
@ -116,7 +162,7 @@ export function LogstashPanel(props) {
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription data-test-subj="lsUptime">
{ formatNumber(props.max_uptime, 'time_since') }
{ props.max_uptime ? formatNumber(props.max_uptime, 'time_since') : 0 }
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
@ -138,7 +184,9 @@ export function LogstashPanel(props) {
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
<EuiLink
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeLogstashData}
onClick={goToPipelines}
data-test-subj="lsPipelines"
aria-label={i18n.translate(
@ -154,7 +202,7 @@ export function LogstashPanel(props) {
defaultMessage="Pipelines: {pipelineCount}"
values={{ pipelineCount: (<span data-test-subj="number_of_logstash_pipelines">{ props.pipeline_count }</span>) }}
/>
</EuiLink>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
</EuiFlexItem>
@ -177,14 +225,14 @@ export function LogstashPanel(props) {
defaultMessage="With Memory Queues"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{ props.queue_types[LOGSTASH.QUEUE_TYPES.MEMORY] }</EuiDescriptionListDescription>
<EuiDescriptionListDescription>{ queueTypes[LOGSTASH.QUEUE_TYPES.MEMORY] || 0 }</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.monitoring.cluster.overview.logstashPanel.withPersistentQueuesLabel"
defaultMessage="With Persistent Queues"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{ props.queue_types[LOGSTASH.QUEUE_TYPES.PERSISTED] }</EuiDescriptionListDescription>
<EuiDescriptionListDescription>{ queueTypes[LOGSTASH.QUEUE_TYPES.PERSISTED] || 0 }</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiPanel>
</EuiFlexItem>

View file

@ -330,6 +330,9 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear
setupMode={setupMode}
uuidField="resolver"
nameField="name"
setupNewButtonLabel={i18n.translate('xpack.monitoring.elasticsearch.metricbeatMigration.setupNewButtonLabel', {
defaultMessage: 'Setup monitoring for new Elasticsearch node'
})}
search={{
box: {
incremental: true,

View file

@ -227,6 +227,9 @@ export class KibanaInstances extends PureComponent {
setupMode={setupMode}
uuidField="kibana.uuid"
nameField="name"
setupNewButtonLabel={i18n.translate('xpack.monitoring.kibana.metricbeatMigration.setupNewButtonLabel', {
defaultMessage: 'Setup monitoring for new Kibana instance'
})}
search={{
box: {
incremental: true,

View file

@ -55,6 +55,7 @@ exports[`Listing should render with certain data pieces missing 1`] = `
],
}
}
nameField="name"
rows={
Array [
Object {
@ -80,6 +81,8 @@ exports[`Listing should render with certain data pieces missing 1`] = `
},
}
}
setupMode={Object {}}
setupNewButtonLabel="Setup monitoring for new Logstash node"
sorting={
Object {
"sort": Object {
@ -90,6 +93,7 @@ exports[`Listing should render with certain data pieces missing 1`] = `
},
}
}
uuidField="logstash.uuid"
/>
`;
@ -148,6 +152,7 @@ exports[`Listing should render with expected props 1`] = `
],
}
}
nameField="name"
rows={
Array [
Object {
@ -205,6 +210,8 @@ exports[`Listing should render with expected props 1`] = `
},
}
}
setupMode={Object {}}
setupNewButtonLabel="Setup monitoring for new Logstash node"
sorting={
Object {
"sort": Object {
@ -215,5 +222,6 @@ exports[`Listing should render with expected props 1`] = `
},
}
}
uuidField="logstash.uuid"
/>
`;

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PureComponent } from 'react';
import React, { PureComponent, Fragment } from 'react';
import { get } from 'lodash';
import { EuiPage, EuiLink, EuiPageBody, EuiPageContent, EuiPanel, EuiSpacer } from '@elastic/eui';
import { EuiPage, EuiLink, EuiPageBody, EuiPageContent, EuiPanel, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { formatPercentageUsage, formatNumber } from '../../../lib/format_number';
import { ClusterStatus } from '..//cluster_status';
import { EuiMonitoringTable } from '../../table';
@ -110,8 +110,9 @@ export class Listing extends PureComponent {
}
];
}
render() {
const { data, stats, sorting, pagination, onTableChange } = this.props;
const { stats, sorting, pagination, onTableChange, data, setupMode } = this.props;
const columns = this.getColumns();
const flattenedData = data.map(item => ({
...item,
@ -123,6 +124,29 @@ export class Listing extends PureComponent {
version: get(item, 'logstash.version', 'N/A'),
}));
let netNewUserMessage = null;
if (setupMode.enabled && setupMode.data && get(setupMode.data, 'detected.mightExist')) {
netNewUserMessage = (
<Fragment>
<EuiCallOut
title={i18n.translate('xpack.monitoring.logstash.nodes.metribeatMigration.netNewUserTitle', {
defaultMessage: 'No monitoring data detected',
})}
color="warning"
iconType="help"
>
<p>
{i18n.translate('xpack.monitoring.logstash.nodes.metribeatMigration.netNewUserDescription', {
defaultMessage: `Based on your indices, we think you might have a Logstash node. Click the 'Setup monitoring'
button below to start monitoring this node.`
})}
</p>
</EuiCallOut>
<EuiSpacer size="m"/>
</Fragment>
);
}
return (
<EuiPage>
<EuiPageBody>
@ -130,10 +154,17 @@ export class Listing extends PureComponent {
<ClusterStatus stats={stats} />
</EuiPanel>
<EuiSpacer size="m" />
{netNewUserMessage}
<EuiPageContent>
<EuiMonitoringTable
className="logstashNodesTable"
rows={flattenedData}
setupMode={setupMode}
uuidField="logstash.uuid"
nameField="name"
setupNewButtonLabel={i18n.translate('xpack.monitoring.logstash.metricbeatMigration.setupNewButtonLabel', {
defaultMessage: 'Setup monitoring for new Logstash node'
})}
columns={columns}
sorting={{
...sorting,

View file

@ -60,7 +60,8 @@ describe('Listing', () => {
},
sorting: {
sort: 'asc'
}
},
setupMode: {}
};
const component = shallow(<Listing {...props} />);
@ -79,7 +80,8 @@ describe('Listing', () => {
},
sorting: {
sort: 'asc'
}
},
setupMode: {}
};
const component = shallow(<Listing {...props} />);

View file

@ -20,19 +20,23 @@ import {
EuiButtonEmpty,
EuiLink,
EuiText,
EuiCallOut,
EuiSpacer,
EuiCheckbox,
} from '@elastic/eui';
import { getInstructionSteps } from '../instruction_steps';
import { Storage } from 'ui/storage';
import { STORAGE_KEY, ELASTICSEARCH_CUSTOM_ID } from '../../../../common/constants';
import { ensureMinimumTime } from '../../../lib/ensure_minimum_time';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { get } from 'lodash';
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 { KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../../../../telemetry/common/constants';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
import { setNewlyDiscoveredClusterUuid } from '../../../lib/setup_mode';
@ -65,6 +69,7 @@ export class Flyout extends Component {
[INSTRUCTION_STEP_DISABLE_INTERNAL]: false,
},
checkingMigrationStatus: false,
userAcknowledgedNoClusterUuidPrompt: false
};
}
@ -101,7 +106,9 @@ export class Flyout extends Component {
checkForMigrationStatus = async () => {
this.setState({ checkingMigrationStatus: true });
await ensureMinimumTime(this.props.updateProduct(), 1000);
await ensureMinimumTime(
this.props.updateProduct(this.props.instance.uuid, true), 1000
);
this.setState(state => ({
...state,
checkingMigrationStatus: false,
@ -177,7 +184,7 @@ export class Flyout extends Component {
renderActiveStepNextButton() {
const { product, productName } = this.props;
const { activeStep, esMonitoringUrl } = this.state;
const { activeStep, esMonitoringUrl, userAcknowledgedNoClusterUuidPrompt } = 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
@ -205,6 +212,19 @@ export class Flyout extends Component {
}
}
// This is a possible scenario that come up during testing where logstash/beats
// is not outputing to ES, but has monitorining enabled. In these scenarios,
// the monitoring documents will not have a `cluster_uuid` so once migrated,
// the instance/node will actually live in the standalone cluster listing
// instead of the one it currently lives in. We need the user to understand
// this so we're going to force them to acknowledge a prompt saying this
if (product.isFullyMigrated && product.clusterUuid === null) {
// Did they acknowledge the prompt?
if (!userAcknowledgedNoClusterUuidPrompt) {
willDisableDoneButton = true;
}
}
if (willShowNextButton) {
let isDisabled = false;
let nextStep = null;
@ -237,7 +257,6 @@ export class Flyout extends Component {
</EuiButton>
);
}
return (
<EuiButton
type="submit"
@ -318,6 +337,71 @@ export class Flyout extends Component {
});
}
let noClusterUuidPrompt = null;
if (product.isFullyMigrated && product.clusterUuid === null) {
const nodeText = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.node', {
defaultMessage: 'node'
});
const instanceText = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.instance', {
defaultMessage: 'instance'
});
let typeText = nodeText;
if (productName === BEATS_SYSTEM_ID) {
typeText = instanceText;
}
noClusterUuidPrompt = (
<Fragment>
<EuiCallOut
color="warning"
iconType="help"
title={i18n.translate(
'xpack.monitoring.metricbeatMigration.flyout.noClusterUuidTitle',
{
defaultMessage: 'No cluster detected'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.flyout.noClusterUuidDescription"
defaultMessage="This {productName} {typeText} is not connected to an Elasticsearch cluster so once fully migrated,
this {productName} {typeText} will appear in the Standalone cluster instead of this one. {link}"
values={{
productName,
typeText,
link: (
<EuiLink href={`#/overview?_g=(cluster_uuid:__standalone_cluster__)`} target="_blank">
Click here to view the Standalone cluster.
</EuiLink>
)
}}
/>
</p>
<EuiSpacer size="s"/>
<EuiCheckbox
id="monitoringFlyoutNoClusterUuidCheckbox"
label={i18n.translate(
'xpack.monitoring.metricbeatMigration.flyout.noClusterUuidCheckboxLabel',
{
defaultMessage: `Yes, I understand that I will need to look in the Standalone cluster for
this {productName} {typeText}.`,
values: {
productName,
typeText
}
}
)}
checked={this.state.userAcknowledgedNoClusterUuidPrompt}
onChange={e => this.setState({ userAcknowledgedNoClusterUuidPrompt: e.target.checked })}
/>
</EuiCallOut>
<EuiSpacer size="s"/>
</Fragment>
);
}
return (
<EuiFlyout
onClose={onClose}
@ -333,6 +417,7 @@ export class Flyout extends Component {
</EuiFlyoutHeader>
<EuiFlyoutBody>
{this.renderActiveStep()}
{noClusterUuidPrompt}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">

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.apmInstructions.statusTitle', {
defaultMessage: `Migration status`
});

View file

@ -0,0 +1,197 @@
/*
* 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_apm_instructions';
export function getApmInstructionsForDisablingInternalCollection(product, meta, {
checkForMigrationStatus,
checkingMigrationStatus,
hasCheckedStatus,
autoCheckIntervalInMs,
}) {
const disableInternalCollectionStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.title', {
defaultMessage: 'Disable internal collection of the APM server\'s monitoring metrics'
}),
children: (
<Fragment>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.description"
defaultMessage="Add the following setting in the APM server's configuration file ({file}):"
values={{
file: (
<Monospace>apm-server.yml</Monospace>
)
}}
/>
</p>
</EuiText>
<EuiSpacer size="s"/>
<EuiCodeBlock
isCopyable
language="bash"
>
monitoring.enabled: false
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.note"
defaultMessage="You'll need to restart the APM server after making this change."
/>
</p>
</EuiText>
</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.apmInstructions.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.apmInstructions.partiallyMigratedStatusTitle',
{
defaultMessage: `We still see data coming from internal collection of this APM server.`
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.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.apmInstructions.disableInternalCollection.checkingStatusButtonLabel',
{
defaultMessage: 'Checking...'
}
);
} else {
buttonLabel = i18n.translate(
'xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.checkStatusButtonLabel',
{
defaultMessage: 'Check'
}
);
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate(
'xpack.monitoring.metricbeatMigration.apmInstructions.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.apmInstructions.disableInternalCollection.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.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_apm_instructions';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
export function getApmInstructionsForEnablingMetricbeat(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.apmInstructions.metricbeatSecuritySetup"
defaultMessage="If security features are enabled, there may be more setup required.{link}"
values={{
link: (
<Fragment>
{` `}
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/apm/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.metricbeatSecuritySetupLinkText"
defaultMessage="View more information."
/>
</EuiLink>
</Fragment>
)
}}
/>
</EuiText>
)}
/>
</Fragment>
);
const installMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatTitle', {
defaultMessage: 'Install Metricbeat on the same server as the APM server'
}),
children: (
<EuiText>
<p>
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/apm/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
const enableMetricbeatModuleStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleTitle', {
defaultMessage: 'Enable and configure the Beat x-pack module in Metricbeat'
}),
children: (
<Fragment>
<EuiCodeBlock
isCopyable
language="bash"
>
metricbeat modules enable beat-xpack
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleDescription"
defaultMessage="By default the module will collect APM server monitoring metrics from http://localhost:5066. If the local APM server 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/beat-xpack.yml</Monospace>
)
}}
/>
</p>
</EuiText>
{securitySetup}
</Fragment>
)
};
const configureMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatTitle', {
defaultMessage: 'Configure Metricbeat to send to the monitoring cluster'
}),
children: (
<Fragment>
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.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.apmInstructions.startMetricbeatTitle', {
defaultMessage: 'Start Metricbeat'
}),
children: (
<EuiText>
<p>
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/apm/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
let migrationStatusStep = null;
if (product.isInternalCollector || product.isNetNewUser) {
let status = null;
if (hasCheckedStatus) {
status = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.isInternalCollectorStatusTitle', {
defaultMessage: `We have not detected any monitoring data coming from Metricbeat for this APM server.
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.apmInstructions.checkingStatusButtonLabel', {
defaultMessage: 'Checking for data...'
});
} else {
buttonLabel = i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.checkStatusButtonLabel', {
defaultMessage: 'Check for data'
});
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.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.apmInstructions.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.apmInstructions.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 { getApmInstructionsForDisablingInternalCollection } from './disable_internal_collection_instructions';
export { getApmInstructionsForEnablingMetricbeat } from './enable_metricbeat_instructions';

View file

@ -0,0 +1,13 @@
/*
* 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.beatsInstructions.statusTitle', {
defaultMessage: `Migration status`
});
export const UNDETECTED_BEAT_TYPE = 'beat';
export const DEFAULT_BEAT_FOR_URLS = 'metricbeat';

View file

@ -0,0 +1,205 @@
/*
* 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, UNDETECTED_BEAT_TYPE } from './common_beats_instructions';
export function getBeatsInstructionsForDisablingInternalCollection(product, meta, {
checkForMigrationStatus,
checkingMigrationStatus,
hasCheckedStatus,
autoCheckIntervalInMs,
}) {
const beatType = product.beatType;
const disableInternalCollectionStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.title', {
defaultMessage: 'Disable internal collection of {beatType}\'s monitoring metrics',
values: {
beatType: beatType || UNDETECTED_BEAT_TYPE
}
}),
children: (
<Fragment>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.description"
defaultMessage="Add the following setting in {beatType}'s configuration file ({file}):"
values={{
beatType: beatType || UNDETECTED_BEAT_TYPE,
file: (
<Monospace>{beatType}.yml</Monospace>
)
}}
/>
</p>
</EuiText>
<EuiSpacer size="s"/>
<EuiCodeBlock
isCopyable
language="bash"
>
monitoring.enabled: false
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.note"
defaultMessage="You'll need to restart {beatType} after making this change."
values={{
beatType: beatType || UNDETECTED_BEAT_TYPE
}}
/>
</p>
</EuiText>
</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.beatsInstructions.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.beatsInstructions.partiallyMigratedStatusTitle',
{
defaultMessage: `We still see data coming from internal collection of this beat.`
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.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.beatsInstructions.disableInternalCollection.checkingStatusButtonLabel',
{
defaultMessage: 'Checking...'
}
);
} else {
buttonLabel = i18n.translate(
'xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.checkStatusButtonLabel',
{
defaultMessage: 'Check'
}
);
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate(
'xpack.monitoring.metricbeatMigration.beatsInstructions.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.beatsInstructions.disableInternalCollection.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.fullyMigratedStatusDescription"
defaultMessage="We are not seeing any documents from internal collection. Migration complete!"
/>
</p>
</EuiCallOut>
)
};
}
return [
disableInternalCollectionStep,
migrationStatusStep
];
}

View file

@ -0,0 +1,307 @@
/*
* 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, UNDETECTED_BEAT_TYPE, DEFAULT_BEAT_FOR_URLS } from './common_beats_instructions';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, {
esMonitoringUrl,
hasCheckedStatus,
checkingMigrationStatus,
checkForMigrationStatus,
autoCheckIntervalInMs
}) {
const beatType = product.beatType;
const securitySetup = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
color="warning"
iconType="help"
title={(
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.metricbeatSecuritySetup"
defaultMessage="If security features are enabled, there may be more setup required.{link}"
values={{
link: (
<Fragment>
{` `}
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/beats/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.metricbeatSecuritySetupLinkText"
defaultMessage="View more information."
/>
</EuiLink>
</Fragment>
)
}}
/>
</EuiText>
)}
/>
</Fragment>
);
const installMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatTitle', {
defaultMessage: 'Install Metricbeat on the same server as this {beatType}',
values: {
beatType: beatType || UNDETECTED_BEAT_TYPE
}
}),
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.beatsInstructions.installMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
const httpEndpointUrl = `${ELASTIC_WEBSITE_URL}guide/en/beats/${beatType || DEFAULT_BEAT_FOR_URLS}`
+ `/${DOC_LINK_VERSION}/http-endpoint.html`;
const enableMetricbeatModuleStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleTitle', {
defaultMessage: 'Enable and configure the Beat x-pack module in Metricbeat'
}),
children: (
<Fragment>
<EuiCodeBlock
isCopyable
language="bash"
>
metricbeat modules enable beat-xpack
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleDescription"
defaultMessage="By default the module will collect {beatType} monitoring metrics from http://localhost:5066. If the {beatType} instance being monitored 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/beat-xpack.yml</Monospace>
),
beatType: beatType || UNDETECTED_BEAT_TYPE
}}
/>
</p>
</EuiText>
<EuiSpacer size="m"/>
<EuiCallOut
color="warning"
iconType="help"
title={(
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirections"
defaultMessage="In order for Metricbeat to collect metrics from the running {beatType}, you need to {link}."
values={{
link: (
<EuiLink
href={httpEndpointUrl}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirectionsLinkText"
defaultMessage="enable an HTTP endpoint for the {beatType} instance being monitored"
values={{
beatType
}}
/>
</EuiLink>
),
beatType: beatType || UNDETECTED_BEAT_TYPE
}}
/>
</p>
</EuiText>
)}
/>
{securitySetup}
</Fragment>
)
};
const configureMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatTitle', {
defaultMessage: 'Configure Metricbeat to send to the monitoring cluster'
}),
children: (
<Fragment>
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.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.beatsInstructions.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.beatsInstructions.startMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
let migrationStatusStep = null;
if (product.isInternalCollector || product.isNetNewUser) {
let status = null;
if (hasCheckedStatus) {
status = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.isInternalCollectorStatusTitle', {
defaultMessage: `We have not detected any monitoring data coming from Metricbeat for this Beat.
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.beatsInstructions.checkingStatusButtonLabel', {
defaultMessage: 'Checking for data...'
});
} else {
buttonLabel = i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.checkStatusButtonLabel', {
defaultMessage: 'Check for data'
});
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.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.beatsInstructions.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.beatsInstructions.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 { getBeatsInstructionsForDisablingInternalCollection } from './disable_internal_collection_instructions';
export { getBeatsInstructionsForEnablingMetricbeat } from './enable_metricbeat_instructions';

View file

@ -12,27 +12,62 @@ import {
getElasticsearchInstructionsForEnablingMetricbeat,
getElasticsearchInstructionsForDisablingInternalCollection
} from './elasticsearch';
import {
getLogstashInstructionsForEnablingMetricbeat,
getLogstashInstructionsForDisablingInternalCollection,
} from './logstash';
import {
getBeatsInstructionsForEnablingMetricbeat,
getBeatsInstructionsForDisablingInternalCollection,
} from './beats';
import {
getApmInstructionsForEnablingMetricbeat,
getApmInstructionsForDisablingInternalCollection,
} from './apm';
import {
INSTRUCTION_STEP_ENABLE_METRICBEAT,
INSTRUCTION_STEP_DISABLE_INTERNAL
} from '../constants';
import { ELASTICSEARCH_CUSTOM_ID, APM_CUSTOM_ID } from '../../../../common/constants';
import { KIBANA_SYSTEM_ID, LOGSTASH_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../../../../telemetry/common/constants';
export function getInstructionSteps(productName, product, step, meta, opts) {
switch (productName) {
case 'kibana':
case KIBANA_SYSTEM_ID:
if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
return getKibanaInstructionsForEnablingMetricbeat(product, meta, opts);
}
if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) {
return getKibanaInstructionsForDisablingInternalCollection(product, meta, opts);
}
case 'elasticsearch':
case ELASTICSEARCH_CUSTOM_ID:
if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
return getElasticsearchInstructionsForEnablingMetricbeat(product, meta, opts);
}
if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) {
return getElasticsearchInstructionsForDisablingInternalCollection(product, meta, opts);
}
case LOGSTASH_SYSTEM_ID:
if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
return getLogstashInstructionsForEnablingMetricbeat(product, meta, opts);
}
if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) {
return getLogstashInstructionsForDisablingInternalCollection(product, meta, opts);
}
case BEATS_SYSTEM_ID:
if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
return getBeatsInstructionsForEnablingMetricbeat(product, meta, opts);
}
if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) {
return getBeatsInstructionsForDisablingInternalCollection(product, meta, opts);
}
case APM_CUSTOM_ID:
if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) {
return getApmInstructionsForEnablingMetricbeat(product, meta, opts);
}
if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) {
return getApmInstructionsForDisablingInternalCollection(product, meta, opts);
}
}
return [];
}

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.logstashInstructions.statusTitle', {
defaultMessage: `Migration status`
});

View file

@ -0,0 +1,197 @@
/*
* 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_logstash_instructions';
export function getLogstashInstructionsForDisablingInternalCollection(product, meta, {
checkForMigrationStatus,
checkingMigrationStatus,
hasCheckedStatus,
autoCheckIntervalInMs,
}) {
const disableInternalCollectionStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.title', {
defaultMessage: 'Disable internal collection of Logstash monitoring metrics'
}),
children: (
<Fragment>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.description"
defaultMessage="Add the following setting in the Logstash configuration file ({file}):"
values={{
file: (
<Monospace>logstash.yml</Monospace>
)
}}
/>
</p>
</EuiText>
<EuiSpacer size="s"/>
<EuiCodeBlock
isCopyable
language="bash"
>
xpack.monitoring.enabled: false
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.note"
defaultMessage="You'll need to restart Logstash after making this change."
/>
</p>
</EuiText>
</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.logstashInstructions.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.logstashInstructions.partiallyMigratedStatusTitle',
{
defaultMessage: `We still see data coming from internal collection of Logstash.`
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.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.logstashInstructions.disableInternalCollection.checkingStatusButtonLabel',
{
defaultMessage: 'Checking...'
}
);
} else {
buttonLabel = i18n.translate(
'xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.checkStatusButtonLabel',
{
defaultMessage: 'Check'
}
);
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate(
'xpack.monitoring.metricbeatMigration.logstashInstructions.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.logstashInstructions.disableInternalCollection.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.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_logstash_instructions';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
export function getLogstashInstructionsForEnablingMetricbeat(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.logstashInstructions.metricbeatSecuritySetup"
defaultMessage="If security features are enabled, there may be more setup required.{link}"
values={{
link: (
<Fragment>
{` `}
<EuiLink
href={`${ELASTIC_WEBSITE_URL}guide/en/logstash/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html`}
target="_blank"
>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.metricbeatSecuritySetupLinkText"
defaultMessage="View more information."
/>
</EuiLink>
</Fragment>
)
}}
/>
</EuiText>
)}
/>
</Fragment>
);
const installMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatTitle', {
defaultMessage: 'Install Metricbeat on the same server as Logstash'
}),
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.logstashInstructions.installMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
const enableMetricbeatModuleStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleTitle', {
defaultMessage: 'Enable and configure the Logstash x-pack module in Metricbeat'
}),
children: (
<Fragment>
<EuiCodeBlock
isCopyable
language="bash"
>
metricbeat modules enable logstash-xpack
</EuiCodeBlock>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleDescription"
defaultMessage="By default the module will collect Logstash monitoring metrics from http://localhost:9600. If the local Logstash 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/logstash-xpack.yml</Monospace>
)
}}
/>
</p>
</EuiText>
{securitySetup}
</Fragment>
)
};
const configureMetricbeatStep = {
title: i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatTitle', {
defaultMessage: 'Configure Metricbeat to send to the monitoring cluster'
}),
children: (
<Fragment>
<EuiText>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.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.logstashInstructions.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.logstashInstructions.startMetricbeatLinkText"
defaultMessage="Follow the instructions here"
/>
</EuiLink>
</p>
</EuiText>
)
};
let migrationStatusStep = null;
if (product.isInternalCollector || product.isNetNewUser) {
let status = null;
if (hasCheckedStatus) {
status = (
<Fragment>
<EuiSpacer size="m"/>
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.isInternalCollectorStatusTitle', {
defaultMessage: `We have not detected any monitoring data coming from Metricbeat for this Logstash.
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.logstashInstructions.checkingStatusButtonLabel', {
defaultMessage: 'Checking for data...'
});
} else {
buttonLabel = i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.checkStatusButtonLabel', {
defaultMessage: 'Check for data'
});
}
migrationStatusStep = {
title: statusTitle,
status: 'incomplete',
children: (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.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.logstashInstructions.fullyMigratedStatusTitle',
{
defaultMessage: 'Congratulations!'
}
)}
>
<p>
<FormattedMessage
id="xpack.monitoring.metricbeatMigration.logstashInstructions.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 { getLogstashInstructionsForDisablingInternalCollection } from './disable_internal_collection_instructions';
export { getLogstashInstructionsForEnablingMetricbeat } from './enable_metricbeat_instructions';

View file

@ -0,0 +1,13 @@
/*
* 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 function findNewUuid(oldUuids, newUuids) {
for (const newUuid of newUuids) {
if (oldUuids.indexOf(newUuid) === -1) {
return newUuid;
}
}
}

View file

@ -6,42 +6,93 @@
import React from 'react';
import { getSetupModeState, initSetupModeState, updateSetupModeData } from '../../lib/setup_mode';
import { Flyout } from '../metricbeat_migration/flyout';
import { ELASTICSEARCH_CUSTOM_ID } from '../../../common/constants';
import { findNewUuid } from './lib/find_new_uuid';
export class SetupModeRenderer extends React.Component {
state = {
renderState: false,
isFlyoutOpen: false,
instance: null,
newProduct: null,
isSettingUpNew: false,
}
componentWillMount() {
const { scope, injector } = this.props;
initSetupModeState(scope, injector, () => this.setState({ renderState: true }));
initSetupModeState(scope, injector, (_oldData) => {
const newState = { renderState: true };
const { productName } = this.props;
if (!productName) {
this.setState(newState);
return;
}
const setupModeState = getSetupModeState();
if (!setupModeState.enabled || !setupModeState.data) {
this.setState(newState);
return;
}
const data = setupModeState.data[productName];
const oldData = _oldData ? _oldData[productName] : null;
if (data && oldData) {
const newUuid = findNewUuid(Object.keys(oldData.byUuid), Object.keys(data.byUuid));
if (newUuid) {
newState.newProduct = data.byUuid[newUuid];
}
}
this.setState(newState);
});
}
reset() {
this.setState({
renderState: false,
isFlyoutOpen: false,
instance: null,
newProduct: null,
isSettingUpNew: false,
});
}
getFlyout(data, meta) {
const { productName } = this.props;
const { isFlyoutOpen, instance } = this.state;
const { isFlyoutOpen, instance, isSettingUpNew, newProduct } = 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];
let product = null;
if (newProduct) {
product = newProduct;
}
// For new instance discovery flow, we pass in empty instance object
else if (instance && Object.keys(instance).length) {
product = data.byUuid[instance.uuid];
}
if (!product) {
const uuids = Object.values(data.byUuid);
if (uuids.length && !isSettingUpNew) {
product = uuids[0];
}
else {
product = {
isNetNewUser: true
};
}
}
return (
<Flyout
onClose={() => this.setState({ isFlyoutOpen: false })}
onClose={() => this.reset()}
productName={productName}
product={product}
meta={meta}
instance={instance}
updateProduct={updateSetupModeData}
isSettingUpNew={isSettingUpNew}
/>
);
}
@ -59,6 +110,7 @@ export class SetupModeRenderer extends React.Component {
data = setupModeState.data;
}
}
const meta = setupModeState.data ? setupModeState.data._meta : null;
return render({
@ -67,7 +119,7 @@ export class SetupModeRenderer extends React.Component {
enabled: setupModeState.enabled,
productName,
updateSetupModeData,
openFlyout: (instance) => this.setState({ isFlyoutOpen: true, instance }),
openFlyout: (instance, isSettingUpNew) => this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }),
closeFlyout: () => this.setState({ isFlyoutOpen: false }),
},
flyoutComponent: this.getFlyout(data, meta),

View file

@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { Fragment } from 'react';
import { get } from 'lodash';
import {
EuiInMemoryTable,
EuiBadge,
EuiButtonEmpty,
EuiHealth
EuiHealth,
EuiButton,
EuiSpacer
} from '@elastic/eui';
import { ELASTICSEARCH_CUSTOM_ID } from '../../../common/constants';
import { i18n } from '@kbn/i18n';
@ -46,6 +48,7 @@ export class EuiMonitoringTable extends React.PureComponent {
return column;
});
let footerContent = null;
if (setupMode && setupMode.enabled) {
columns.push({
name: i18n.translate('xpack.monitoring.euiTable.setupStatusTitle', {
@ -185,6 +188,15 @@ export class EuiMonitoringTable extends React.PureComponent {
return null;
}
});
footerContent = (
<Fragment>
<EuiSpacer size="m"/>
<EuiButton onClick={() => setupMode.openFlyout({}, true)}>
{props.setupNewButtonLabel}
</EuiButton>
</Fragment>
);
}
return (
@ -195,6 +207,7 @@ export class EuiMonitoringTable extends React.PureComponent {
columns={columns}
{...props}
/>
{footerContent}
</div>
);
}

View file

@ -6,13 +6,18 @@
<div data-transclude-slot="bottomRow">
<div ng-if="monitoringMain.inElasticsearch" class="euiTabs" role="navigation">
<a
ng-if="!monitoringMain.instance"
ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')"
kbn-href="#/elasticsearch"
class="euiTab"
ng-class="{
'euiTab-isSelected': monitoringMain.isActiveTab('overview'),
'euiTab-isDisabled': monitoringMain.isDisabledTab('elasticsearch', 'overview')
}"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.esNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.esNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
@ -25,14 +30,18 @@
i18n-default-message="Nodes"
></a>
<a
ng-if="!monitoringMain.instance"
ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')"
kbn-href="#/elasticsearch/indices"
class="euiTab"
ng-disabled="true"
ng-class="{
'euiTab-isSelected': monitoringMain.isActiveTab('indices'),
'euiTab-isDisabled': monitoringMain.isDisabledTab('elasticsearch', 'indices')
}"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}"
i18n-id="xpack.monitoring.esNavigation.indicesLinkText"
i18n-default-message="Indices"
></a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}"
i18n-id="xpack.monitoring.esNavigation.indicesLinkText"
i18n-default-message="Indices"
></a>
@ -81,13 +90,23 @@
<div ng-if="monitoringMain.inKibana" class="euiTabs" role="navigation">
<a
ng-if="!monitoringMain.instance"
ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('kibana')"
kbn-href="#/kibana"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.kibanaNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('kibana')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{
'euiTab-isSelected': monitoringMain.isActiveTab('overview'),
}"
i18n-id="xpack.monitoring.kibanaNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
<a
ng-if="!monitoringMain.instance"
kbn-href="#/kibana/instances"
@ -101,13 +120,21 @@
<div ng-if="monitoringMain.inApm" class="euiTabs" role="navigation">
<a
ng-if="!monitoringMain.instance"
ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('apm')"
kbn-href="#/apm"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.apmNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('apm')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.apmNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
<a
ng-if="!monitoringMain.instance"
kbn-href="#/apm/instances"
@ -121,11 +148,19 @@
<div ng-if="monitoringMain.inBeats" class="euiTabs" role="navigation">
<a
ng-if="!monitoringMain.instance"
ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('beats')"
kbn-href="#/beats"
class="euiTab"
ng-class="{'euiTab-isSelected':
monitoringMain.isActiveTab('overview')}"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.beatsNavigation.overviewLinkText"
i18n-default-message="Overview"
>
</a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('beats')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.beatsNavigation.overviewLinkText"
i18n-default-message="Overview"
>
@ -152,7 +187,7 @@
<div ng-if="monitoringMain.inLogstash" class="euiTabs" role="navigation">
<a
ng-if="!monitoringMain.instance && !monitoringMain.pipelineId"
ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && !monitoringMain.isDisabledTab('logstash')"
kbn-href="#/logstash"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
@ -160,6 +195,15 @@
i18n-default-message="Overview"
>
</a>
<a
ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && monitoringMain.isDisabledTab('logstash')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.logstashNavigation.overviewLinkText"
i18n-default-message="Overview"
>
</a>
<a
ng-if="!monitoringMain.instance && !monitoringMain.pipelineId"
kbn-href="#/logstash/nodes"
@ -170,7 +214,7 @@
>
</a>
<a
ng-if="!monitoringMain.instance && !monitoringMain.pipelineId"
ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && !monitoringMain.isDisabledTab('logstash')"
kbn-href="#/logstash/pipelines"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('pipelines')}"
@ -181,6 +225,18 @@
></span>
<span class="kuiIcon fa-flask monTabs--icon" tooltip="Beta feature" />
</a>
<a
ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && monitoringMain.isDisabledTab('logstash')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('pipelines')}"
>
<span
i18n-id="xpack.monitoring.logstashNavigation.pipelinesLinkText"
i18n-default-message="Pipelines"
></span>
<span class="kuiIcon fa-flask monTabs--icon" tooltip="Beta feature" />
</a>
<a
ng-if="monitoringMain.instance"
kbn-href="#/logstash/node/{{ monitoringMain.resolver }}"

View file

@ -5,6 +5,7 @@
*/
import { ajaxErrorHandlersProvider } from './ajax_error_handler';
import { get } from 'lodash';
const angularState = {
injector: null,
@ -36,7 +37,7 @@ export const setNewlyDiscoveredClusterUuid = clusterUuid => {
executor.run();
};
export const fetchCollectionData = async () => {
export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) => {
checkAngularState();
const http = angularState.injector.get('$http');
@ -45,8 +46,11 @@ export const fetchCollectionData = async () => {
const ccs = globalState.ccs;
let url = '../api/monitoring/v1/setup/collection';
if (clusterUuid) {
url += `/${clusterUuid}`;
if (uuid) {
url += `/node/${uuid}`;
}
else if (!fetchWithoutClusterUuid && clusterUuid) {
url += `/cluster/${clusterUuid}`;
}
try {
@ -60,13 +64,17 @@ export const fetchCollectionData = async () => {
}
};
const notifySetupModeDataChange = () => {
setupModeState.callbacks.forEach(cb => cb());
const notifySetupModeDataChange = (oldData) => {
setupModeState.callbacks.forEach(cb => cb(oldData));
};
export const updateSetupModeData = async () => {
setupModeState.data = await fetchCollectionData();
notifySetupModeDataChange();
export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) => {
const oldData = setupModeState.data;
setupModeState.data = await fetchCollectionData(uuid, fetchWithoutClusterUuid);
if (get(setupModeState.data, '_meta.isOnCloud', false)) {
return toggleSetupMode(false); // eslint-disable-line no-use-before-define
}
notifySetupModeDataChange(oldData);
};
export const toggleSetupMode = inSetupMode => {

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,8 @@ import template from './index.html';
import { ApmServerInstances } from '../../../components/apm/instances';
import { MonitoringViewBaseEuiTableController } from '../..';
import { I18nContext } from 'ui/i18n';
import { SetupModeRenderer } from '../../../components/renderers';
import { APM_CUSTOM_ID } from '../../../../common/constants';
uiRoutes.when('/apm/instances', {
template,
@ -45,6 +47,9 @@ uiRoutes.when('/apm/instances', {
$injector
});
this.scope = $scope;
this.injector = $injector;
$scope.$watch(() => this.data, data => {
this.renderReact(data);
});
@ -59,13 +64,24 @@ uiRoutes.when('/apm/instances', {
const component = (
<I18nContext>
<ApmServerInstances
apms={{
pagination,
sorting,
onTableChange,
data,
}}
<SetupModeRenderer
scope={this.scope}
injector={this.injector}
productName={APM_CUSTOM_ID}
render={({ setupMode, flyoutComponent }) => (
<Fragment>
{flyoutComponent}
<ApmServerInstances
setupMode={setupMode}
apms={{
pagination,
sorting,
onTableChange,
data,
}}
/>
</Fragment>
)}
/>
</I18nContext>
);

View file

@ -11,9 +11,11 @@ import { routeInitProvider } from 'plugins/monitoring/lib/route_init';
import { MonitoringViewBaseEuiTableController } from '../../';
import { getPageData } from './get_page_data';
import template from './index.html';
import React from 'react';
import React, { Fragment } from 'react';
import { I18nContext } from 'ui/i18n';
import { Listing } from '../../../components/beats/listing/listing';
import { SetupModeRenderer } from '../../../components/renderers';
import { BEATS_SYSTEM_ID } from '../../../../../telemetry/common/constants';
uiRoutes.when('/beats/beats', {
template,
@ -43,6 +45,7 @@ uiRoutes.when('/beats/beats', {
this.data = $route.current.locals.pageData;
this.scope = $scope;
this.injector = $injector;
this.kbnUrl = $injector.get('kbnUrl');
//Bypassing super.updateData, since this controller loads its own data
@ -55,16 +58,27 @@ uiRoutes.when('/beats/beats', {
const { sorting, pagination, onTableChange } = this.scope.beats;
this.renderReact(
<I18nContext>
<Listing
stats={this.data.stats}
data={this.data.listing}
sorting={this.sorting || sorting}
pagination={this.pagination || pagination}
onTableChange={this.onTableChange || onTableChange}
angular={{
kbnUrl: this.kbnUrl,
scope: this.scope,
}}
<SetupModeRenderer
scope={this.scope}
injector={this.injector}
productName={BEATS_SYSTEM_ID}
render={({ setupMode, flyoutComponent }) => (
<Fragment>
{flyoutComponent}
<Listing
stats={this.data.stats}
data={this.data.listing}
setupMode={setupMode}
sorting={this.sorting || sorting}
pagination={this.pagination || pagination}
onTableChange={this.onTableChange || onTableChange}
angular={{
kbnUrl: this.kbnUrl,
scope: this.scope,
}}
/>
</Fragment>
)}
/>
</I18nContext>
);

View file

@ -16,6 +16,7 @@ import { ElasticsearchNodes } from '../../../components';
import { I18nContext } from 'ui/i18n';
import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler';
import { SetupModeRenderer } from '../../../components/renderers';
import { ELASTICSEARCH_CUSTOM_ID } from '../../../../common/constants';
uiRoutes.when('/elasticsearch/nodes', {
template,
@ -82,7 +83,7 @@ uiRoutes.when('/elasticsearch/nodes', {
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName="elasticsearch"
productName={ELASTICSEARCH_CUSTOM_ID}
render={({ setupMode, flyoutComponent }) => (
<Fragment>
{flyoutComponent}

View file

@ -13,6 +13,7 @@ import template from './index.html';
import { KibanaInstances } from 'plugins/monitoring/components/kibana/instances';
import { SetupModeRenderer } from '../../../components/renderers';
import { I18nContext } from 'ui/i18n';
import { KIBANA_SYSTEM_ID } from '../../../../../telemetry/common/constants';
uiRoutes.when('/kibana/instances', {
template,
@ -44,7 +45,7 @@ uiRoutes.when('/kibana/instances', {
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName="kibana"
productName={KIBANA_SYSTEM_ID}
render={({ setupMode, flyoutComponent }) => (
<Fragment>
{flyoutComponent}

View file

@ -3,7 +3,7 @@
* 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 React, { Fragment } from 'react';
import uiRoutes from'ui/routes';
import { routeInitProvider } from 'plugins/monitoring/lib/route_init';
import { MonitoringViewBaseEuiTableController } from '../../';
@ -11,6 +11,8 @@ import { getPageData } from './get_page_data';
import template from './index.html';
import { I18nContext } from 'ui/i18n';
import { Listing } from '../../../components/logstash/listing';
import { SetupModeRenderer } from '../../../components/renderers';
import { LOGSTASH_SYSTEM_ID } from '../../../../../telemetry/common/constants';
uiRoutes.when('/logstash/nodes', {
template,
@ -39,14 +41,26 @@ uiRoutes.when('/logstash/nodes', {
$scope.$watch(() => this.data, data => {
this.renderReact(
<I18nContext>
<Listing
data={data.nodes}
stats={data.clusterStatus}
sorting={this.sorting}
pagination={this.pagination}
onTableChange={this.onTableChange}
angular={{ kbnUrl, scope: $scope }}
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName={LOGSTASH_SYSTEM_ID}
render={({ setupMode, flyoutComponent }) => (
<Fragment>
{flyoutComponent}
<Listing
data={data.nodes}
setupMode={setupMode}
stats={data.clusterStatus}
sorting={this.sorting}
pagination={this.pagination}
onTableChange={this.onTableChange}
angular={{ kbnUrl, scope: $scope }}
/>
</Fragment>
)}
/>
</I18nContext>
);
});

View file

@ -5,15 +5,14 @@
*/
import { get, uniq } from 'lodash';
import { METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, ELASTICSEARCH_CUSTOM_ID } from '../../../../common/constants';
import { METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, ELASTICSEARCH_CUSTOM_ID, APM_CUSTOM_ID } from '../../../../common/constants';
import { KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID, LOGSTASH_SYSTEM_ID } from '../../../../../telemetry/common/constants';
import { getLivesNodes } from '../../elasticsearch/nodes/get_nodes/get_live_nodes';
import { KIBANA_STATS_TYPE } from '../../../../../../../../src/legacy/server/status/constants';
const NUMBER_OF_SECONDS_AGO_TO_LOOK = 30;
const APM_CUSTOM_ID = 'apm';
const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid) => {
const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nodeUuid) => {
const start = get(req.payload, 'timeRange.min', `now-${NUMBER_OF_SECONDS_AGO_TO_LOOK}s`);
const end = get(req.payload, 'timeRange.max', 'now');
@ -32,6 +31,20 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid) =>
filters.push({ term: { 'cluster_uuid': clusterUuid } });
}
const nodesClause = [];
if (nodeUuid) {
nodesClause.push({
bool: {
should: [
{ term: { 'node_stats.node_id': nodeUuid } },
{ term: { 'kibana_stats.kibana.uuid': nodeUuid } },
{ term: { 'beats_stats.beat.uuid': nodeUuid } },
{ term: { 'logstash_stats.logstash.uuid': nodeUuid } }
]
}
});
}
const params = {
index: Object.values(indexPatterns),
size: 0,
@ -43,6 +56,7 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid) =>
query: {
bool: {
filter: filters,
must: nodesClause,
}
},
aggs: {
@ -90,6 +104,11 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid) =>
terms: {
field: 'beats_stats.beat.type'
}
},
cluster_uuid: {
terms: {
field: 'cluster_uuid'
}
}
}
},
@ -102,6 +121,11 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid) =>
max: {
field: 'timestamp'
}
},
cluster_uuid: {
terms: {
field: 'cluster_uuid'
}
}
}
}
@ -268,8 +292,10 @@ async function getLiveElasticsearchClusterUuid(req) {
* @param {*} req Standard request object. Can contain a timeRange to use for the query
* @param {*} indexPatterns Map of index patterns to search against (will be all .monitoring-* indices)
* @param {*} clusterUuid Optional and will be used to filter down the query if used
* @param {*} nodeUuid Optional and will be used to filter down the query if used
* @param {*} skipLiveData Optional and will not make any live api calls if set to true
*/
export const getCollectionStatus = async (req, indexPatterns, clusterUuid, skipLiveData) => {
export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeUuid, skipLiveData) => {
const config = req.server.config();
const kibanaUuid = config.get('server.uuid');
@ -285,7 +311,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, skipL
recentDocuments,
detectedProducts
] = await Promise.all([
await getRecentMonitoringDocuments(req, indexPatterns, clusterUuid),
await getRecentMonitoringDocuments(req, indexPatterns, clusterUuid, nodeUuid),
await detectProducts(req)
]);
@ -355,6 +381,12 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, skipL
if (product.name === KIBANA_SYSTEM_ID && key === kibanaUuid) {
map[key].isPrimary = true;
}
if (product.name === BEATS_SYSTEM_ID) {
map[key].beatType = get(bucket.beat_type, 'buckets[0].key');
}
if (bucket.cluster_uuid) {
map[key].clusterUuid = get(bucket.cluster_uuid, 'buckets[0].key', '') || null;
}
}
}
productStatus.totalUniqueInstanceCount = Object.keys(map).length;
@ -416,6 +448,12 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, skipL
if (product.name === KIBANA_SYSTEM_ID && key === kibanaUuid) {
map[key].isPrimary = true;
}
if (product.name === BEATS_SYSTEM_ID) {
map[key].beatType = get(bucket.beat_type, 'buckets[0].key');
}
if (bucket.cluster_uuid) {
map[key].clusterUuid = get(bucket.cluster_uuid, 'buckets[0].key', '') || null;
}
}
}
if (!isFullyMigrated) {
@ -473,6 +511,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, skipL
status._meta = {
secondsAgo: NUMBER_OF_SECONDS_AGO_TO_LOOK,
clusterUuid: liveClusterUuid,
isOnCloud: get(req.server.plugins, 'cloud.config.isCloudEnabled', false)
};
return status;

View file

@ -16,11 +16,11 @@ export function clusterSetupStatusRoute(server) {
*/
server.route({
method: 'POST',
path: '/api/monitoring/v1/setup/collection/{clusterUuid}',
path: '/api/monitoring/v1/setup/collection/cluster/{clusterUuid}',
config: {
validate: {
params: Joi.object({
clusterUuid: Joi.string().required()
clusterUuid: Joi.string().required(),
}),
query: Joi.object({
// This flag is not intended to be used in production. It was introduced
@ -50,7 +50,7 @@ export function clusterSetupStatusRoute(server) {
try {
await verifyMonitoringAuth(req);
const indexPatterns = getIndexPatterns(server);
status = await getCollectionStatus(req, indexPatterns, req.params.clusterUuid, req.query.skipLiveData);
status = await getCollectionStatus(req, indexPatterns, req.params.clusterUuid, null, req.query.skipLiveData);
} catch (err) {
throw handleError(err, req);
}

View file

@ -47,7 +47,7 @@ export function clustersSetupStatusRoute(server) {
try {
await verifyMonitoringAuth(req);
const indexPatterns = getIndexPatterns(server);
status = await getCollectionStatus(req, indexPatterns, null, req.query.skipLiveData);
status = await getCollectionStatus(req, indexPatterns, null, null, req.query.skipLiveData);
} catch (err) {
throw handleError(err, req);
}

View file

@ -6,3 +6,4 @@
export { clustersSetupStatusRoute } from './clusters_setup_status';
export { clusterSetupStatusRoute } from './cluster_setup_status';
export { nodeSetupStatusRoute } from './node_setup_status';

View file

@ -0,0 +1,61 @@
/*
* 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 Joi from 'joi';
import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
import { handleError } from '../../../../lib/errors';
import { getCollectionStatus } from '../../../../lib/setup/collection';
import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
export function nodeSetupStatusRoute(server) {
/*
* Monitoring Home
* Route Init (for checking license and compatibility for multi-cluster monitoring
*/
server.route({
method: 'POST',
path: '/api/monitoring/v1/setup/collection/node/{nodeUuid}',
config: {
validate: {
params: Joi.object({
nodeUuid: Joi.string().required(),
}),
query: Joi.object({
// This flag is not intended to be used in production. It was introduced
// as a way to ensure consistent API testing - the typical data source
// for API tests are archived data, where the cluster configuration and data
// are consistent from environment to environment. However, this endpoint
// also attempts to retrieve data from the running stack products (ES and Kibana)
// which will vary from environment to environment making it difficult
// to write tests against. Therefore, this flag exists and should only be used
// in our testing environment.
skipLiveData: Joi.boolean().default(false)
}),
payload: Joi.object({
timeRange: Joi.object({
min: Joi.date().required(),
max: Joi.date().required()
}).optional()
}).allow(null)
}
},
handler: async (req) => {
let status = null;
// NOTE using try/catch because checkMonitoringAuth is expected to throw
// an error when current logged-in user doesn't have permission to read
// the monitoring data. `try/catch` makes it a little more explicit.
try {
await verifyMonitoringAuth(req);
const indexPatterns = getIndexPatterns(server);
status = await getCollectionStatus(req, indexPatterns, null, req.params.nodeUuid, req.query.skipLiveData);
} catch (err) {
throw handleError(err, req);
}
return status;
}
});
}

View file

@ -46,6 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -55,6 +55,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -46,6 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -46,6 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -46,6 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -20,6 +20,8 @@
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
"beatType": "metricbeat",
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"lastTimestamp": 1554821586714,
"isInternalCollector": true,
"isNetNewUser": false
@ -34,6 +36,7 @@
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
"lastTimestamp": 1554821579833,
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"isInternalCollector": true,
"isNetNewUser": false
}
@ -61,6 +64,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -20,6 +20,8 @@
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
"beatType": "metricbeat",
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"lastTimestamp": 1554821406717,
"isInternalCollector": true,
"isNetNewUser": false
@ -34,6 +36,7 @@
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
"lastTimestamp": 1554821409656,
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"isInternalCollector": true,
"isNetNewUser": false
}
@ -61,6 +64,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -20,6 +20,8 @@
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
"beatType": "metricbeat",
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"lastTimestamp": 1554821536716,
"isInternalCollector": true,
"isNetNewUser": false
@ -34,6 +36,7 @@
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
"lastTimestamp": 1554821539784,
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"isInternalCollector": true,
"isNetNewUser": false
}
@ -61,6 +64,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}

View file

@ -20,6 +20,8 @@
"detected": null,
"byUuid": {
"8eba4902-df80-43b0-b6c2-ed8ca290984e": {
"beatType": "metricbeat",
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"lastTimestamp": 1554821354041,
"isInternalCollector": true,
"isNetNewUser": false
@ -34,6 +36,7 @@
"byUuid": {
"4134a00e-89e4-4896-a3d4-c3a9aa03a594": {
"lastTimestamp": 1554821359616,
"clusterUuid": "jJ9oySUPR8G0wuV82vG5-g",
"isInternalCollector": true,
"isNetNewUser": false
}
@ -61,6 +64,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"clusterUuid": null
}
}