[7.x] [Monitoring] Introducing Logs UI (#31275) (#35069)

* [Monitoring] Introducing Logs UI (#31275)

* Initial implementation

* More logs UI work

* Remove unnecessary code

* Add support to build a logs url based on the cluster and/or node uuid

* Deep link directly

* Update link

* Use CCS to access remote filebeat data

* Fix existing tests

* Add log specific api integration tests

* Localization

* Adding more localization

* Adding unit tests for logs ui

* Client side unit tests, configuration for log fetch count, adding visual callout for why we can't detect logs

* Remove debug

* Fix localization issue

* Update tests

* PR feedback

* Update import

* Format the count to avoid a huge string of numbers

* Use @timestamp instead

* Handle scenario where the time period is not right but the type exists

* Update jest tests

* Update api tests

* Text changes

* Add periods

* Update tests

* Update jest snapshot (#35082)
This commit is contained in:
Chris Roberson 2019-04-15 12:43:14 -04:00 committed by GitHub
parent f3e982e02f
commit e87682c5f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 6470 additions and 40 deletions

View file

@ -159,3 +159,5 @@ export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logs
export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*';
export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7';
export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*';
export const INDEX_PATTERN_FILEBEAT = 'filebeat-*';

View file

@ -70,7 +70,8 @@ export const config = (Joi) => {
keyPassphrase: Joi.string(),
alwaysPresentCertificate: Joi.boolean().default(false),
}).default(),
apiVersion: Joi.string().default('master')
apiVersion: Joi.string().default('master'),
logFetchCount: Joi.number().default(10)
}).default(),
tests: Joi.object({
cloud_detector: Joi.object({

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { get } from 'lodash';
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 {
@ -18,9 +18,14 @@ import {
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiHorizontalRule,
EuiBadge,
EuiToolTip,
EuiFlexGroup,
} from '@elastic/eui';
import { LicenseText } from './license_text';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { Reason } from '../../logs/reason';
const calculateShards = shards => {
const total = get(shards, 'total', 0);
@ -40,8 +45,97 @@ const calculateShards = shards => {
};
};
function ElasticsearchPanelUi(props) {
function getBadgeColorFromLogLevel(level) {
switch (level) {
case 'warn':
return 'warning';
case 'debug':
return 'hollow';
case 'info':
return 'default';
case 'error':
return 'danger';
}
}
function renderLogs(props) {
if (!props.logs.enabled) {
return (
<EuiDescriptionList>
<Reason reason={props.logs.reason}/>
</EuiDescriptionList>
);
}
return (
<EuiDescriptionList type="column">
{props.logs.types.map((log, index) => (
<Fragment key={index}>
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.monitoring.cluster.overview.logsPanel.logTypeTitle"
defaultMessage="{type}"
values={{
type: capitalize(log.type),
}}
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{renderLog(log)}
</EuiDescriptionListDescription>
</Fragment>
))}
{props.logs.types.length === 0
? (
<FormattedMessage
id="xpack.monitoring.cluster.overview.logsPanel.noLogsFound"
defaultMessage="No logs found."
/>
)
: null
}
</EuiDescriptionList>
);
}
const logLevelText = {
info: i18n.translate('xpack.monitoring.cluster.overview.esPanel.infoLogsTooltipText', {
defaultMessage: 'The number of information logs'
}),
warn: i18n.translate('xpack.monitoring.cluster.overview.esPanel.warnLogsTooltipText', {
defaultMessage: 'The number of warning logs'
}),
debug: i18n.translate('xpack.monitoring.cluster.overview.esPanel.debugLogsTooltipText', {
defaultMessage: 'The number of debug logs'
}),
error: i18n.translate('xpack.monitoring.cluster.overview.esPanel.errorLogsTooltipText', {
defaultMessage: 'The number of error logs'
}),
fatal: i18n.translate('xpack.monitoring.cluster.overview.esPanel.fatalLogsTooltipText', {
defaultMessage: 'The number of fatal logs'
}),
};
function renderLog(log) {
return (
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
{log.levels.map((level, index) => (
<EuiFlexItem grow={false} key={index}>
<EuiToolTip
position="top"
content={logLevelText[level.level]}
>
<EuiBadge color={getBadgeColorFromLogLevel(level.level)}>
{formatNumber(level.count, 'int_commas')}
</EuiBadge>
</EuiToolTip>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
}
function ElasticsearchPanelUi(props) {
const clusterStats = props.cluster_stats || {};
const nodes = clusterStats.nodes;
const indices = clusterStats.indices;
@ -239,6 +333,28 @@ function ElasticsearchPanelUi(props) {
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<EuiLink
onClick={goToElasticsearch}
aria-label={props.intl.formatMessage({
id: 'xpack.monitoring.cluster.overview.esPanel.logsLinkAriaLabel', defaultMessage: 'Elasticsearch Logs' })}
data-test-subj="esLogs"
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.esPanel.logsLinkLabel"
defaultMessage="Logs"
/>
</EuiLink>
</h3>
</EuiTitle>
<EuiHorizontalRule margin="m" />
{renderLogs(props)}
</EuiPanel>
</EuiFlexItem>
</EuiFlexGrid>
</ClusterItemContainer>
);

View file

@ -17,11 +17,15 @@ import {
import { IndexDetailStatus } from '../index_detail_status';
import { MonitoringTimeseriesContainer } from '../../chart';
import { ShardAllocation } from '../shard_allocation/shard_allocation';
import { Logs } from '../../logs';
export const Index = ({
scope,
indexSummary,
metrics,
clusterUuid,
indexUuid,
logs,
kbnUrl,
...props
}) => {
@ -54,6 +58,10 @@ export const Index = ({
))}
</EuiFlexGrid>
<EuiSpacer size="m"/>
<EuiPanel>
<Logs logs={logs} indexUuid={indexUuid} clusterUuid={clusterUuid} />
</EuiPanel>
<EuiSpacer size="m"/>
<ShardAllocation scope={scope} kbnUrl={kbnUrl} type="index" />
</EuiPageContent>
</EuiPageBody>

View file

@ -15,12 +15,16 @@ import {
EuiPanel,
} from '@elastic/eui';
import { NodeDetailStatus } from '../node_detail_status';
import { Logs } from '../../logs/';
import { MonitoringTimeseriesContainer } from '../../chart';
import { ShardAllocation } from '../shard_allocation/shard_allocation';
export const Node = ({
nodeSummary,
metrics,
logs,
nodeId,
clusterUuid,
scope,
kbnUrl,
...props
@ -53,9 +57,15 @@ export const Node = ({
</EuiFlexItem>
))}
</EuiFlexGrid>
<EuiSpacer size="m"/>
<ShardAllocation scope={scope} kbnUrl={kbnUrl}/>
</EuiPageContent>
<EuiSpacer size="m"/>
<EuiPanel>
<Logs logs={logs} nodeId={nodeId} clusterUuid={clusterUuid} />
</EuiPanel>
<EuiSpacer size="m"/>
<EuiPanel>
<ShardAllocation scope={scope} kbnUrl={kbnUrl}/>
</EuiPanel>
</EuiPageBody>
</EuiPage>
);

View file

@ -9,10 +9,13 @@ import { ClusterStatus } from '../cluster_status';
import { ShardActivity } from '../shard_activity';
import { MonitoringTimeseriesContainer } from '../../chart';
import { EuiPage, EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer, EuiPageBody, EuiPageContent } from '@elastic/eui';
import { Logs } from '../../logs/logs';
export function ElasticsearchOverview({
clusterStatus,
metrics,
logs,
cluster,
shardActivity,
...props
}) {
@ -42,8 +45,15 @@ export function ElasticsearchOverview({
</EuiFlexItem>
))}
</EuiFlexGrid>
<ShardActivity data={shardActivity} {...props} />
</EuiPageContent>
<EuiSpacer size="m" />
<EuiPanel>
<Logs logs={logs} clusterUuid={cluster.cluster_uuid}/>
</EuiPanel>
<EuiSpacer size="m" />
<EuiPanel>
<ShardActivity data={shardActivity} {...props} />
</EuiPanel>
</EuiPageBody>
</EuiPage>
);

View file

@ -0,0 +1,319 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Logs should render a link to filter by cluster uuid 1`] = `
<EuiCallOut
color="primary"
iconType="loggingApp"
size="m"
title="Want to see more logs?"
>
<p>
<FormattedMessage
defaultMessage="Visit {link} to dive deeper."
id="xpack.monitoring.logs.listing.linkText"
values={
Object {
"link": <EuiLink
color="primary"
href="/app/infra#/link-to/logs?filter=elasticsearch.cluster.uuid:12345"
type="button"
>
Logs
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render a link to filter by cluster uuid and index uuid 1`] = `
<EuiCallOut
color="primary"
iconType="loggingApp"
size="m"
title="Want to see more logs?"
>
<p>
<FormattedMessage
defaultMessage="Visit {link} to dive deeper."
id="xpack.monitoring.logs.listing.linkText"
values={
Object {
"link": <EuiLink
color="primary"
href="/app/infra#/link-to/logs?filter=elasticsearch.cluster.uuid:12345 and elasticsearch.index.name:6789"
type="button"
>
Logs
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render a link to filter by cluster uuid and node uuid 1`] = `
<EuiCallOut
color="primary"
iconType="loggingApp"
size="m"
title="Want to see more logs?"
>
<p>
<FormattedMessage
defaultMessage="Visit {link} to dive deeper."
id="xpack.monitoring.logs.listing.linkText"
values={
Object {
"link": <EuiLink
color="primary"
href="/app/infra#/link-to/logs?filter=elasticsearch.cluster.uuid:12345 and elasticsearch.node.id:6789"
type="button"
>
Logs
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render a reason if the logs are disabled 1`] = `
<div>
<EuiTitle
size="m"
textTransform="none"
>
<h1>
Recent Logs
</h1>
</EuiTitle>
<EuiText
grow={true}
size="s"
>
<p>
Showing the most recent logs for this cluster, up to 15 total logs.
</p>
</EuiText>
<EuiSpacer
size="m"
/>
<Reason
reason={Object {}}
/>
<EuiSpacer
size="m"
/>
</div>
`;
exports[`Logs should render fewer columns for node or index view 1`] = `
Array [
Object {
"field": "timestamp",
"name": "Timestamp",
"render": [Function],
"width": "12%",
},
Object {
"field": "level",
"name": "Level",
"width": "5%",
},
Object {
"field": "type",
"name": "Type",
"render": [Function],
"width": "10%",
},
Object {
"field": "message",
"name": "Message",
"width": "55%",
},
Object {
"field": "component",
"name": "Component",
"width": "18%",
},
]
`;
exports[`Logs should render normally 1`] = `
<div>
<EuiTitle
size="m"
textTransform="none"
>
<h1>
Recent Logs
</h1>
</EuiTitle>
<EuiText
grow={true}
size="s"
>
<p>
Showing the most recent logs for this cluster, up to 10 total logs.
</p>
</EuiText>
<EuiSpacer
size="m"
/>
<EuiBasicTable
columns={
Array [
Object {
"field": "timestamp",
"name": "Timestamp",
"render": [Function],
"width": "12%",
},
Object {
"field": "level",
"name": "Level",
"width": "5%",
},
Object {
"field": "type",
"name": "Type",
"render": [Function],
"width": "10%",
},
Object {
"field": "message",
"name": "Message",
"width": "45%",
},
Object {
"field": "component",
"name": "Component",
"width": "15%",
},
Object {
"field": "node",
"name": "Node",
"width": "13%",
},
]
}
items={
Array [
Object {
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"level": "WARN",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.",
"node": "foobar",
"timestamp": "2019-03-18T12:49:33.783Z",
"type": "deprecation",
},
Object {
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"level": "WARN",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.",
"node": "foobar",
"timestamp": "2019-03-18T12:49:26.781Z",
"type": "deprecation",
},
Object {
"component": "o.e.c.r.a.DiskThresholdMonitor",
"level": "WARN",
"message": "high disk watermark [90%] exceeded on [-pH5RhfsRl6FDeTPwD5vEw][Elastic-MBP.local][/Users/chris/Development/repos/kibana/.es/8.0.0/data/nodes/0] free: 29.5gb[6.3%], shards will be relocated away from this node",
"node": "foobar2",
"timestamp": "2019-03-18T12:49:24.414Z",
"type": "server",
},
Object {
"component": "o.e.c.r.a.DiskThresholdMonitor",
"level": "INFO",
"message": "rerouting shards: [high disk watermark exceeded on one or more nodes]",
"node": "foobar",
"timestamp": "2019-03-18T12:49:24.414Z",
"type": "server",
},
Object {
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"level": "WARN",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.",
"node": "foobar",
"timestamp": "2019-03-18T12:49:11.776Z",
"type": "deprecation",
},
Object {
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"level": "WARN",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.",
"node": "foobar",
"timestamp": "2019-03-18T12:49:08.770Z",
"type": "deprecation",
},
Object {
"component": "o.e.c.r.a.DiskThresholdMonitor",
"level": "WARN",
"message": "high disk watermark [90%] exceeded on [-pH5RhfsRl6FDeTPwD5vEw][Elastic-MBP.local][/Users/chris/Development/repos/kibana/.es/8.0.0/data/nodes/0] free: 29.3gb[6.2%], shards will be relocated away from this node",
"node": "foobar",
"timestamp": "2019-03-18T12:48:59.409Z",
"type": "server",
},
Object {
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"level": "WARN",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.",
"node": "foobar",
"timestamp": "2019-03-18T12:48:53.753Z",
"type": "deprecation",
},
Object {
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"level": "WARN",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.",
"node": "foobar",
"timestamp": "2019-03-18T12:48:53.753Z",
"type": "deprecation",
},
Object {
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"level": "WARN",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.",
"node": "foobar2",
"timestamp": "2019-03-18T12:48:46.745Z",
"type": "deprecation",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
<EuiSpacer
size="m"
/>
<EuiCallOut
color="primary"
iconType="loggingApp"
size="m"
title="Want to see more logs?"
>
<p>
<FormattedMessage
defaultMessage="Visit {link} to dive deeper."
id="xpack.monitoring.logs.listing.linkText"
values={
Object {
"link": <EuiLink
color="primary"
href="/app/infra#/link-to/logs"
type="button"
>
Logs
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
</div>
`;

View file

@ -0,0 +1,170 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Logs should render with a no cluster found reason 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title="No logs for this cluster"
>
<p>
<FormattedMessage
defaultMessage="Check that your {link} is correct."
id="xpack.monitoring.logs.reason.noClusterMessage"
values={
Object {
"link": <EuiLink
color="primary"
href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html"
type="button"
>
setup
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render with a no index found reason 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title="No logs for this index"
>
<p>
<FormattedMessage
defaultMessage="We found logs, but none for this index. If this problem continues, check that your {link} is correct."
id="xpack.monitoring.logs.reason.noIndexMessage"
values={
Object {
"link": <EuiLink
color="primary"
href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html"
type="button"
>
setup
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render with a no index pattern found reason 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title="No log data found"
>
<p>
<FormattedMessage
defaultMessage="Set up {link}, then configure your Elasticsearch output to your monitoring cluster."
id="xpack.monitoring.logs.reason.noIndexPatternMessage"
values={
Object {
"link": <EuiLink
color="primary"
href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html"
type="button"
>
Filebeat
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render with a no node found reason 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title="No logs for this Elasticsearch node"
>
<p>
<FormattedMessage
defaultMessage="Check that your {link} is correct."
id="xpack.monitoring.logs.reason.noNodeMessage"
values={
Object {
"link": <EuiLink
color="primary"
href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html"
type="button"
>
setup
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render with a no type found reason 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title="No logs for Elasticsearch"
>
<p>
<FormattedMessage
defaultMessage="Follow {link} to set up Elasticsearch."
id="xpack.monitoring.logs.reason.noTypeMessage"
values={
Object {
"link": <EuiLink
color="primary"
href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-elasticsearch.html"
type="button"
>
these directions
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render with a time period reason 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title="No logs for the selected time"
>
<p>
<FormattedMessage
defaultMessage="Use the time filter to adjust your timeframe."
id="xpack.monitoring.logs.reason.noIndexPatternInTimePeriodMessage"
values={Object {}}
/>
</p>
</EuiCallOut>
`;
exports[`Logs should render with a time period reason for both scenarios 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title="No logs for the selected time"
>
<p>
<FormattedMessage
defaultMessage="Use the time filter to adjust your timeframe."
id="xpack.monitoring.logs.reason.noIndexPatternInTimePeriodMessage"
values={Object {}}
/>
</p>
</EuiCallOut>
`;

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { Logs } from './logs';

View file

@ -0,0 +1,242 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PureComponent } from 'react';
import { capitalize } from 'lodash';
import chrome from 'ui/chrome';
import {
EuiBasicTable,
EuiTitle,
EuiSpacer,
EuiText,
EuiCallOut,
EuiLink,
} from '@elastic/eui';
import { formatDateTimeLocal } from '../../../common/formatting';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Reason } from './reason';
const columnTimestampTitle = i18n.translate('xpack.monitoring.logs.listing.timestampTitle', {
defaultMessage: 'Timestamp'
});
const columnLevelTitle = i18n.translate('xpack.monitoring.logs.listing.levelTitle', {
defaultMessage: 'Level'
});
const columnTypeTitle = i18n.translate('xpack.monitoring.logs.listing.typeTitle', {
defaultMessage: 'Type'
});
const columnMessageTitle = i18n.translate('xpack.monitoring.logs.listing.messageTitle', {
defaultMessage: 'Message'
});
const columnComponentTitle = i18n.translate('xpack.monitoring.logs.listing.componentTitle', {
defaultMessage: 'Component'
});
const columnNodeTitle = i18n.translate('xpack.monitoring.logs.listing.nodeTitle', {
defaultMessage: 'Node'
});
const columns = [
{
field: 'timestamp',
name: columnTimestampTitle,
width: '12%',
render: timestamp => formatDateTimeLocal(timestamp),
},
{
field: 'level',
name: columnLevelTitle,
width: '5%',
},
{
field: 'type',
name: columnTypeTitle,
width: '10%',
render: type => capitalize(type),
},
{
field: 'message',
name: columnMessageTitle,
width: '55%'
},
{
field: 'component',
name: columnComponentTitle,
width: '18%'
},
];
const clusterColumns = [
{
field: 'timestamp',
name: columnTimestampTitle,
width: '12%',
render: timestamp => formatDateTimeLocal(timestamp),
},
{
field: 'level',
name: columnLevelTitle,
width: '5%',
},
{
field: 'type',
name: columnTypeTitle,
width: '10%',
render: type => capitalize(type),
},
{
field: 'message',
name: columnMessageTitle,
width: '45%'
},
{
field: 'component',
name: columnComponentTitle,
width: '15%'
},
{
field: 'node',
name: columnNodeTitle,
width: '13%'
},
];
function getLogsUiLink(clusterUuid, nodeId, indexUuid) {
const base = `${chrome.getBasePath()}/app/infra#/link-to/logs`;
const params = [];
if (clusterUuid) {
params.push(`elasticsearch.cluster.uuid:${clusterUuid}`);
}
if (nodeId) {
params.push(`elasticsearch.node.id:${nodeId}`);
}
if (indexUuid) {
params.push(`elasticsearch.index.name:${indexUuid}`);
}
if (params.length === 0) {
return base;
}
return `${base}?filter=${params.join(' and ')}`;
}
export class Logs extends PureComponent {
renderLogs() {
const { logs: { enabled, logs }, nodeId, indexUuid } = this.props;
if (!enabled) {
return null;
}
return (
<EuiBasicTable
items={logs || []}
columns={nodeId || indexUuid ? columns : clusterColumns}
/>
);
}
renderNoLogs() {
const { logs: { enabled, reason } } = this.props;
if (enabled) {
return null;
}
return <Reason reason={reason}/>;
}
renderCallout() {
const { logs: { enabled }, nodeId, clusterUuid, indexUuid } = this.props;
if (!enabled) {
return null;
}
return (
<EuiCallOut
size="m"
title={i18n.translate('xpack.monitoring.logs.listing.calloutTitle', {
defaultMessage: 'Want to see more logs?'
})}
iconType="loggingApp"
>
<p>
<FormattedMessage
id="xpack.monitoring.logs.listing.linkText"
defaultMessage="Visit {link} to dive deeper."
values={{
link: (
<EuiLink href={getLogsUiLink(clusterUuid, nodeId, indexUuid)}>
{i18n.translate('xpack.monitoring.logs.listing.calloutLinkText', {
defaultMessage: 'Logs'
})}
</EuiLink>
)
}}
/>
</p>
</EuiCallOut>
);
}
render() {
const { nodeId, indexUuid, logs: { limit } } = this.props;
let description;
if (nodeId) {
description = i18n.translate('xpack.monitoring.logs.listing.nodePageDescription', {
defaultMessage: 'Showing the most recent logs for this node, up to {limit} total logs.',
values: {
limit,
}
});
}
else if (indexUuid) {
description = i18n.translate('xpack.monitoring.logs.listing.indexPageDescription', {
defaultMessage: 'Showing the most recent logs for this index, up to {limit} total logs.',
values: {
limit,
}
});
}
else {
description = i18n.translate('xpack.monitoring.logs.listing.clusterPageDescription', {
defaultMessage: 'Showing the most recent logs for this cluster, up to {limit} total logs.',
values: {
limit,
}
});
}
return (
<div>
<EuiTitle>
<h1>
{i18n.translate('xpack.monitoring.logs.listing.pageTitle', {
defaultMessage: 'Recent Logs'
})}
</h1>
</EuiTitle>
<EuiText size="s">
<p>
{description}
</p>
</EuiText>
<EuiSpacer size="m"/>
{this.renderLogs()}
{this.renderNoLogs()}
<EuiSpacer size="m"/>
{this.renderCallout()}
</div>
);
}
}

View file

@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { Logs } from './logs';
jest.mock('ui/chrome', () => {
return {
getBasePath: () => ''
};
});
const logs = {
enabled: true,
limit: 10,
logs: [
{
'timestamp': '2019-03-18T12:49:33.783Z',
'component': 'o.e.d.x.m.r.a.RestMonitoringBulkAction',
'level': 'WARN',
'type': 'deprecation',
'node': 'foobar',
'message': '[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.'
}, {
'timestamp': '2019-03-18T12:49:26.781Z',
'component': 'o.e.d.x.m.r.a.RestMonitoringBulkAction',
'level': 'WARN',
'type': 'deprecation',
'node': 'foobar',
'message': '[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.'
}, {
'timestamp': '2019-03-18T12:49:24.414Z',
'component': 'o.e.c.r.a.DiskThresholdMonitor',
'level': 'WARN',
'type': 'server',
'node': 'foobar2',
'message': 'high disk watermark [90%] exceeded on [-pH5RhfsRl6FDeTPwD5vEw][Elastic-MBP.local][/Users/chris/Development/repos/kibana/.es/8.0.0/data/nodes/0] free: 29.5gb[6.3%], shards will be relocated away from this node' // eslint-disable-line max-len
}, {
'timestamp': '2019-03-18T12:49:24.414Z',
'component': 'o.e.c.r.a.DiskThresholdMonitor',
'level': 'INFO',
'type': 'server',
'node': 'foobar',
'message': 'rerouting shards: [high disk watermark exceeded on one or more nodes]'
}, {
'timestamp': '2019-03-18T12:49:11.776Z',
'component': 'o.e.d.x.m.r.a.RestMonitoringBulkAction',
'level': 'WARN',
'type': 'deprecation',
'node': 'foobar',
'message': '[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.'
}, {
'timestamp': '2019-03-18T12:49:08.770Z',
'component': 'o.e.d.x.m.r.a.RestMonitoringBulkAction',
'level': 'WARN',
'type': 'deprecation',
'node': 'foobar',
'message': '[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.'
}, {
'timestamp': '2019-03-18T12:48:59.409Z',
'component': 'o.e.c.r.a.DiskThresholdMonitor',
'level': 'WARN',
'type': 'server',
'node': 'foobar',
'message': 'high disk watermark [90%] exceeded on [-pH5RhfsRl6FDeTPwD5vEw][Elastic-MBP.local][/Users/chris/Development/repos/kibana/.es/8.0.0/data/nodes/0] free: 29.3gb[6.2%], shards will be relocated away from this node' // eslint-disable-line max-len
}, {
'timestamp': '2019-03-18T12:48:53.753Z',
'component': 'o.e.d.x.m.r.a.RestMonitoringBulkAction',
'level': 'WARN',
'type': 'deprecation',
'node': 'foobar',
'message': '[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.'
}, {
'timestamp': '2019-03-18T12:48:53.753Z',
'component': 'o.e.d.x.m.r.a.RestMonitoringBulkAction',
'level': 'WARN',
'type': 'deprecation',
'node': 'foobar',
'message': '[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.'
}, {
'timestamp': '2019-03-18T12:48:46.745Z',
'component': 'o.e.d.x.m.r.a.RestMonitoringBulkAction',
'level': 'WARN',
'type': 'deprecation',
'node': 'foobar2',
'message': '[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead.'
}
]
};
describe('Logs', () => {
it('should render normally', () => {
const component = shallow(<Logs logs={logs}/>);
expect(component).toMatchSnapshot();
});
it('should render fewer columns for node or index view', () => {
const component = shallow(<Logs logs={logs} nodeId="12345"/>);
expect(component.find('EuiBasicTable').prop('columns')).toMatchSnapshot();
});
it('should render a link to filter by cluster uuid', () => {
const component = shallow(<Logs logs={logs} clusterUuid="12345"/>);
expect(component.find('EuiCallOut')).toMatchSnapshot();
});
it('should render a link to filter by cluster uuid and node uuid', () => {
const component = shallow(<Logs logs={logs} clusterUuid="12345" nodeId="6789"/>);
expect(component.find('EuiCallOut')).toMatchSnapshot();
});
it('should render a link to filter by cluster uuid and index uuid', () => {
const component = shallow(<Logs logs={logs} clusterUuid="12345" indexUuid="6789"/>);
expect(component.find('EuiCallOut')).toMatchSnapshot();
});
it('should render a reason if the logs are disabled', () => {
const component = shallow(<Logs logs={{ enabled: false, limit: 15, reason: {} }}/>);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiCallOut,
EuiLink
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
export const Reason = ({ reason }) => {
let title;
let message;
if (false === reason.indexPatternExists) {
title = i18n.translate('xpack.monitoring.logs.reason.noIndexPatternTitle', {
defaultMessage: 'No log data found'
});
message = (
<FormattedMessage
id="xpack.monitoring.logs.reason.noIndexPatternMessage"
defaultMessage="Set up {link}, then configure your Elasticsearch output to your monitoring cluster."
values={{
link: (
<EuiLink href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html">
{i18n.translate('xpack.monitoring.logs.reason.noIndexPatternLink', {
defaultMessage: 'Filebeat'
})}
</EuiLink>
)
}}
/>
);
}
else if (false === reason.indexPatternInTimeRangeExists || (false === reason.typeExists && reason.typeExistsAtAnyTime)) {
title = i18n.translate('xpack.monitoring.logs.reason.noIndexPatternInTimePeriodTitle', {
defaultMessage: 'No logs for the selected time'
});
message = (
<FormattedMessage
id="xpack.monitoring.logs.reason.noIndexPatternInTimePeriodMessage"
defaultMessage="Use the time filter to adjust your timeframe."
/>
);
}
else if (false === reason.typeExists) {
title = i18n.translate('xpack.monitoring.logs.reason.noTypeTitle', {
defaultMessage: 'No logs for Elasticsearch'
});
message = (
<FormattedMessage
id="xpack.monitoring.logs.reason.noTypeMessage"
defaultMessage="Follow {link} to set up Elasticsearch."
values={{
link: (
<EuiLink href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-elasticsearch.html">
{i18n.translate('xpack.monitoring.logs.reason.noTypeLink', {
defaultMessage: 'these directions'
})}
</EuiLink>
)
}}
/>
);
}
else if (false === reason.clusterExists) {
title = i18n.translate('xpack.monitoring.logs.reason.noClusterTitle', {
defaultMessage: 'No logs for this cluster'
});
message = (
<FormattedMessage
id="xpack.monitoring.logs.reason.noClusterMessage"
defaultMessage="Check that your {link} is correct."
values={{
link: (
<EuiLink href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html">
{i18n.translate('xpack.monitoring.logs.reason.noClusterLink', {
defaultMessage: 'setup'
})}
</EuiLink>
)
}}
/>
);
}
else if (false === reason.nodeExists) {
title = i18n.translate('xpack.monitoring.logs.reason.noNodeTitle', {
defaultMessage: 'No logs for this Elasticsearch node'
});
message = (
<FormattedMessage
id="xpack.monitoring.logs.reason.noNodeMessage"
defaultMessage="Check that your {link} is correct."
values={{
link: (
<EuiLink href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html">
{i18n.translate('xpack.monitoring.logs.reason.noNodeLink', {
defaultMessage: 'setup'
})}
</EuiLink>
)
}}
/>
);
}
else if (false === reason.indexExists) {
title = i18n.translate('xpack.monitoring.logs.reason.noIndexTitle', {
defaultMessage: 'No logs for this index'
});
message = (
<FormattedMessage
id="xpack.monitoring.logs.reason.noIndexMessage"
defaultMessage="We found logs, but none for this index. If this problem continues, check that your {link} is correct."
values={{
link: (
<EuiLink href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html">
{i18n.translate('xpack.monitoring.logs.reason.noNodeLink', {
defaultMessage: 'setup'
})}
</EuiLink>
)
}}
/>
);
}
return (
<EuiCallOut
title={title}
color="warning"
iconType="help"
>
<p>{message}</p>
</EuiCallOut>
);
};

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { Reason } from './reason';
describe('Logs', () => {
it('should render with a no index pattern found reason', () => {
const component = shallow(<Reason reason={{ indexPatternExists: false }}/>);
expect(component).toMatchSnapshot();
});
it('should render with a no type found reason', () => {
const component = shallow(<Reason reason={{ indexPatternExists: true, typeExists: false }}/>);
expect(component).toMatchSnapshot();
});
it('should render with a no cluster found reason', () => {
const component = shallow(<Reason reason={{ indexPatternExists: true, typeExists: true, clusterExists: false }}/>);
expect(component).toMatchSnapshot();
});
it('should render with a no node found reason', () => {
const component = shallow(<Reason reason={{ indexPatternExists: true, typeExists: true, clusterExists: true, nodeExists: false }}/>);
expect(component).toMatchSnapshot();
});
it('should render with a time period reason', () => {
const reason = {
indexPatternExists: true,
indexPatternInTimeRangeExists: false,
};
const component = shallow(<Reason reason={reason}/>);
expect(component).toMatchSnapshot();
});
it('should render with a time period reason for both scenarios', () => {
const reason = {
indexPatternExists: true,
indexPatternInTimeRangeExists: true,
clusterExists: true,
typeExists: false,
typeExistsAtAnyTime: true
};
const component = shallow(<Reason reason={reason}/>);
expect(component).toMatchSnapshot();
});
it('should render with a no index found reason', () => {
const component = shallow(<Reason reason={{
indexPatternExists: true,
typeExists: true,
clusterExists: true,
nodeExists: null,
indexExists: false
}}
/>);
expect(component).toMatchSnapshot();
});
});

View file

@ -90,12 +90,15 @@ uiRoutes.when('/elasticsearch/indices/:index', {
$scope.labels = labels.index;
}
this.renderReact(
<I18nContext>
<Index
scope={$scope}
kbnUrl={kbnUrl}
onBrush={this.onBrush}
indexUuid={this.indexName}
clusterUuid={$scope.cluster.cluster_uuid}
{...data}
/>
</I18nContext>

View file

@ -79,6 +79,8 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
<Node
scope={$scope}
kbnUrl={kbnUrl}
nodeId={this.nodeName}
clusterUuid={$scope.cluster.cluster_uuid}
onBrush={this.onBrush}
{...data}
/>

View file

@ -46,7 +46,7 @@ export class ElasticsearchOverviewController extends MonitoringViewBaseControlle
initScope($scope) {
$scope.$watch(() => this.data, data => {
this.renderReact(data);
this.renderReact(data, $scope.cluster);
});
// HACK to force table to re-render even if data hasn't changed. This
@ -56,8 +56,8 @@ export class ElasticsearchOverviewController extends MonitoringViewBaseControlle
const { data } = this;
const dataWithShardActivityLoading = { ...data, shardActivity: null };
// force shard activity to rerender by manipulating and then re-setting its data prop
this.renderReact(dataWithShardActivityLoading);
this.renderReact(data);
this.renderReact(dataWithShardActivityLoading, $scope.cluster);
this.renderReact(data, $scope.cluster);
});
}
@ -67,15 +67,17 @@ export class ElasticsearchOverviewController extends MonitoringViewBaseControlle
});
}
renderReact(data) {
renderReact(data, cluster) {
// All data needs to originate in this view, and get passed as a prop to the components, for statelessness
const { clusterStatus, metrics, shardActivity } = data;
const { clusterStatus, metrics, shardActivity, logs } = data;
const shardActivityData = shardActivity && this.filterShardActivityData(shardActivity); // no filter on data = null
const component = (
<I18nContext>
<ElasticsearchOverview
clusterStatus={clusterStatus}
metrics={metrics}
logs={logs}
cluster={cluster}
shardActivity={shardActivityData}
onBrush={this.onBrush}
showShardActivityHistory={this.showShardActivityHistory}

View file

@ -73,6 +73,7 @@ Array [
},
"status": "green",
},
"logs": undefined,
},
"isCcrEnabled": undefined,
"isPrimary": true,
@ -176,6 +177,7 @@ Array [
},
"status": "green",
},
"logs": undefined,
},
"isCcrEnabled": undefined,
"isPrimary": false,
@ -284,6 +286,7 @@ Array [
},
"status": "green",
},
"logs": undefined,
},
"isCcrEnabled": undefined,
"isPrimary": false,
@ -387,6 +390,7 @@ Array [
},
"status": "green",
},
"logs": undefined,
},
"isCcrEnabled": undefined,
"isPrimary": false,

View file

@ -22,6 +22,7 @@ import { getApmsForClusters } from '../apm/get_apms_for_clusters';
import { i18n } from '@kbn/i18n';
import { checkCcrEnabled } from '../elasticsearch/ccr';
import { getStandaloneClusterDefinition, hasStandaloneClusters } from '../standalone_clusters';
import { getLogTypes } from '../logs';
/**
* Get all clusters or the cluster associated with {@code clusterUuid} when it is defined.
@ -33,7 +34,8 @@ export async function getClustersFromRequest(req, indexPatterns, { clusterUuid,
lsIndexPattern,
beatsIndexPattern,
apmIndexPattern,
alertsIndex
alertsIndex,
filebeatIndexPattern
} = indexPatterns;
const isStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID;
@ -86,6 +88,8 @@ export async function getClustersFromRequest(req, indexPatterns, { clusterUuid,
if (alerts) {
cluster.alerts = alerts;
}
cluster.logs = await getLogTypes(req, filebeatIndexPattern, { clusterUuid: cluster.cluster_uuid, start, end });
} else if (!isStandaloneCluster) {
// get all clusters
if (!clusters || clusters.length === 0) {

View file

@ -23,6 +23,7 @@ export function getClustersSummary(clusters, kibanaUuid, isCcrEnabled) {
alerts,
ccs,
cluster_settings: clusterSettings,
logs,
} = cluster;
const clusterName = get(clusterSettings, 'cluster.metadata.display_name', cluster.cluster_name);
@ -64,7 +65,8 @@ export function getClustersSummary(clusters, kibanaUuid, isCcrEnabled) {
indices,
nodes,
status
}
},
logs
},
logstash,
kibana: omit(kibana, 'uuids'),
@ -78,7 +80,7 @@ export function getClustersSummary(clusters, kibanaUuid, isCcrEnabled) {
status,
kibana && kibana.status || null
]),
isCcrEnabled
isCcrEnabled,
};
});
}

View file

@ -10,6 +10,32 @@ import moment from 'moment';
import { standaloneClusterFilter } from './standalone_clusters';
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants';
export function createTimeFilter(options) {
const { start, end } = options;
if (!start && !end) {
return null;
}
const timestampField = get(options, 'metric.timestampField');
if (!timestampField) {
throw new MissingRequiredError('metric.timestampField');
}
const timeRangeFilter = {
range: {
[timestampField]: {
format: 'epoch_millis'
}
}
};
if (start) {
timeRangeFilter.range[timestampField].gte = moment.utc(start).valueOf();
}
if (end) {
timeRangeFilter.range[timestampField].lte = moment.utc(end).valueOf();
}
return timeRangeFilter;
}
/*
* Creates the boilerplace for querying monitoring data, including filling in
* document UUIDs, start time and end time, and injecting additional filters.
@ -25,7 +51,7 @@ import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants';
*/
export function createQuery(options) {
options = defaults(options, { filters: [] });
const { type, clusterUuid, uuid, start, end, filters } = options;
const { type, clusterUuid, uuid, filters } = options;
const isFromStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID;
@ -53,22 +79,10 @@ export function createQuery(options) {
if (!timestampField) {
throw new MissingRequiredError('metric.timestampField');
}
const timeRangeFilter = {
range: {
[timestampField]: {
format: 'epoch_millis'
}
}
};
if (start) {
timeRangeFilter.range[timestampField].gte = moment.utc(start).valueOf();
}
if (end) {
timeRangeFilter.range[timestampField].lte = moment.utc(end).valueOf();
}
const timeRangeFilter = createTimeFilter(options);
const combinedFilters = [typeFilter, clusterUuidFilter, uuidFilter, ...filters];
if (end || start) {
if (timeRangeFilter) {
combinedFilters.push(timeRangeFilter);
}

View file

@ -0,0 +1,152 @@
/*
* 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 { createTimeFilter } from '../create_query';
import { get } from 'lodash';
async function doesFilebeatIndexExist(req, filebeatIndexPattern, { start, end, clusterUuid, nodeUuid, indexUuid }) {
const metric = { timestampField: '@timestamp' };
const filter = [
createTimeFilter({ start, end, metric })
];
const typeFilter = { term: { 'service.type': 'elasticsearch' } };
const clusterFilter = { term: { 'elasticsearch.cluster.uuid': clusterUuid } };
const nodeFilter = { term: { 'elasticsearch.node.id': nodeUuid } };
const indexFilter = { term: { 'elasticsearch.index.name': indexUuid } };
const indexPatternExistsQuery = {
query: {
bool: {
filter,
}
},
};
const typeExistsAtAnyTimeQuery = {
query: {
bool: {
filter: [
typeFilter,
]
}
},
};
const typeExistsQuery = {
query: {
bool: {
filter: [
...filter,
typeFilter,
]
}
},
};
const clusterExistsQuery = {
query: {
bool: {
filter: [
...filter,
typeFilter,
clusterFilter
]
}
},
};
const nodeExistsQuery = {
query: {
bool: {
filter: [
...filter,
typeFilter,
clusterFilter,
nodeFilter
]
}
},
};
const indexExistsQuery = {
query: {
bool: {
filter: [
...filter,
typeFilter,
clusterFilter,
indexFilter
]
}
},
};
const defaultParams = {
size: 0,
terminate_after: 1,
};
const body = [
{ index: filebeatIndexPattern },
{ ...defaultParams },
{ index: filebeatIndexPattern },
{ ...defaultParams, ...indexPatternExistsQuery },
{ index: filebeatIndexPattern },
{ ...defaultParams, ...typeExistsAtAnyTimeQuery },
{ index: filebeatIndexPattern },
{ ...defaultParams, ...typeExistsQuery },
];
if (clusterUuid) {
body.push(...[
{ index: filebeatIndexPattern },
{ ...defaultParams, ...clusterExistsQuery },
]);
}
if (nodeUuid) {
body.push(...[
{ index: filebeatIndexPattern },
{ ...defaultParams, ...nodeExistsQuery },
]);
}
if (indexUuid) {
body.push(...[
{ index: filebeatIndexPattern },
{ ...defaultParams, ...indexExistsQuery },
]);
}
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const {
responses: [
indexPatternExistsResponse,
indexPatternExistsInTimeRangeResponse,
typeExistsAtAnyTimeResponse,
typeExistsResponse,
clusterExistsResponse,
nodeExistsResponse,
indexExistsResponse
]
} = await callWithRequest(req, 'msearch', { body });
return {
indexPatternExists: get(indexPatternExistsResponse, 'hits.total.value', 0) > 0,
indexPatternInTimeRangeExists: get(indexPatternExistsInTimeRangeResponse, 'hits.total.value', 0) > 0,
typeExistsAtAnyTime: get(typeExistsAtAnyTimeResponse, 'hits.total.value', 0) > 0,
typeExists: get(typeExistsResponse, 'hits.total.value', 0) > 0,
clusterExists: clusterUuid ? get(clusterExistsResponse, 'hits.total.value', 0) > 0 : null,
nodeExists: nodeUuid ? get(nodeExistsResponse, 'hits.total.value', 0) > 0 : null,
indexExists: indexUuid ? get(indexExistsResponse, 'hits.total.value', 0) > 0 : null,
};
}
export async function detectReason(req, filebeatIndexPattern, opts) {
return await doesFilebeatIndexExist(req, filebeatIndexPattern, opts);
}

View file

@ -0,0 +1,93 @@
/*
* 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 { get } from 'lodash';
import { checkParam } from '../error_missing_required';
import { createTimeFilter } from '../create_query';
import { detectReason } from './detect_reason';
async function handleResponse(response, req, filebeatIndexPattern, { start, end }) {
const result = {
enabled: false,
types: []
};
const typeBuckets = get(response, 'aggregations.types.buckets', []);
if (typeBuckets.length) {
result.enabled = true;
result.types = typeBuckets.map(typeBucket => {
return {
type: typeBucket.key.split('.')[1],
levels: typeBucket.levels.buckets.map(levelBucket => {
return {
level: levelBucket.key.toLowerCase(),
count: levelBucket.doc_count
};
})
};
});
}
else {
result.reason = await detectReason(req, filebeatIndexPattern, { start, end });
}
return result;
}
export async function getLogTypes(req, filebeatIndexPattern, { clusterUuid, nodeUuid, indexUuid, start, end }) {
checkParam(filebeatIndexPattern, 'filebeatIndexPattern in logs/getLogTypes');
const metric = { timestampField: '@timestamp' };
const filter = [
{ term: { 'service.type': 'elasticsearch' } },
createTimeFilter({ start, end, metric })
];
if (clusterUuid) {
filter.push({ term: { 'elasticsearch.cluster.uuid': clusterUuid } });
}
if (nodeUuid) {
filter.push({ term: { 'elasticsearch.node.id': nodeUuid } });
}
if (indexUuid) {
filter.push({ term: { 'elasticsearch.index.name': indexUuid } });
}
const params = {
index: filebeatIndexPattern,
size: 0,
filterPath: [
'aggregations.levels.buckets',
'aggregations.types.buckets',
],
ignoreUnavailable: true,
body: {
sort: { '@timestamp': { order: 'desc' } },
query: {
bool: {
filter,
}
},
aggs: {
types: {
terms: {
field: 'event.dataset'
},
aggs: {
levels: {
terms: {
field: 'log.level'
}
},
}
}
}
}
};
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const response = await callWithRequest(req, 'search', params);
return await handleResponse(response, req, filebeatIndexPattern, { start, end });
}

View file

@ -0,0 +1,91 @@
/*
* 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 { get } from 'lodash';
import { checkParam } from '../error_missing_required';
import { createTimeFilter } from '../create_query';
import { detectReason } from './detect_reason';
async function handleResponse(response, req, filebeatIndexPattern, opts) {
const result = {
enabled: false,
logs: []
};
const hits = get(response, 'hits.hits', []);
if (hits.length) {
result.enabled = true;
result.logs = hits.map(hit => {
const source = hit._source;
const type = get(source, 'event.dataset').split('.')[1];
return {
timestamp: get(source, '@timestamp'),
component: get(source, 'elasticsearch.component'),
node: get(source, 'elasticsearch.node.name'),
index: get(source, 'elasticsearch.index.name'),
level: get(source, 'log.level'),
type,
message: get(source, 'message'),
};
});
}
else {
result.reason = await detectReason(req, filebeatIndexPattern, opts);
}
return result;
}
export async function getLogs(config, req, filebeatIndexPattern, { clusterUuid, nodeUuid, indexUuid, start, end }) {
checkParam(filebeatIndexPattern, 'filebeatIndexPattern in logs/getLogs');
const metric = { timestampField: '@timestamp' };
const filter = [
{ term: { 'service.type': 'elasticsearch' } },
createTimeFilter({ start, end, metric })
];
if (clusterUuid) {
filter.push({ term: { 'elasticsearch.cluster.uuid': clusterUuid } });
}
if (nodeUuid) {
filter.push({ term: { 'elasticsearch.node.id': nodeUuid } });
}
if (indexUuid) {
filter.push({ term: { 'elasticsearch.index.name': indexUuid } });
}
const params = {
index: filebeatIndexPattern,
size: Math.min(50, config.get('xpack.monitoring.elasticsearch.logFetchCount')),
filterPath: [
'hits.hits._source.message',
'hits.hits._source.log.level',
'hits.hits._source.@timestamp',
'hits.hits._source.event.dataset',
'hits.hits._source.elasticsearch.component',
'hits.hits._source.elasticsearch.index.name',
'hits.hits._source.elasticsearch.node.name',
],
ignoreUnavailable: true,
body: {
sort: { '@timestamp': { order: 'desc' } },
query: {
bool: {
filter,
}
}
}
};
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const response = await callWithRequest(req, 'search', params);
const result = await handleResponse(response, req, filebeatIndexPattern, { clusterUuid, nodeUuid, indexUuid, start, end });
return {
...result,
limit: params.size,
};
}

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 { getLogs } from './get_logs';
export { getLogTypes } from './get_log_types';

View file

@ -13,7 +13,8 @@ import {
INDEX_PATTERN_ELASTICSEARCH,
INDEX_PATTERN_LOGSTASH,
INDEX_PATTERN_BEATS,
INDEX_ALERTS
INDEX_ALERTS,
INDEX_PATTERN_FILEBEAT
} from '../../../../../common/constants';
export function clusterRoute(server) {
@ -46,7 +47,16 @@ export function clusterRoute(server) {
const beatsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_BEATS, ccs);
const apmIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_BEATS, ccs);
const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs);
const indexPatterns = { esIndexPattern, kbnIndexPattern, lsIndexPattern, beatsIndexPattern, apmIndexPattern, alertsIndex };
const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, '*');
const indexPatterns = {
esIndexPattern,
kbnIndexPattern,
lsIndexPattern,
beatsIndexPattern,
apmIndexPattern,
alertsIndex,
filebeatIndexPattern
};
const options = {
clusterUuid: req.params.clusterUuid,
start: req.payload.timeRange.min,

View file

@ -14,7 +14,8 @@ import {
INDEX_PATTERN_KIBANA,
INDEX_PATTERN_LOGSTASH,
INDEX_PATTERN_BEATS,
INDEX_ALERTS
INDEX_ALERTS,
INDEX_PATTERN_FILEBEAT
} from '../../../../../common/constants';
export function clustersRoute(server) {
@ -53,7 +54,16 @@ export function clustersRoute(server) {
const beatsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_BEATS, ccs);
const apmIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_BEATS, ccs);
const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs);
const indexPatterns = { esIndexPattern, kbnIndexPattern, lsIndexPattern, beatsIndexPattern, apmIndexPattern, alertsIndex };
const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, ccs);
const indexPatterns = {
esIndexPattern,
kbnIndexPattern,
lsIndexPattern,
beatsIndexPattern,
apmIndexPattern,
alertsIndex,
filebeatIndexPattern
};
clusters = await getClustersFromRequest(req, indexPatterns);
} catch (err) {

View file

@ -13,7 +13,8 @@ import { getShardAllocation, getShardStats } from '../../../../lib/elasticsearch
import { handleError } from '../../../../lib/errors/handle_error';
import { prefixIndexPattern } from '../../../../lib/ccs_utils';
import { metricSet } from './metric_set_index_detail';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants';
import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_FILEBEAT } from '../../../../../common/constants';
import { getLogs } from '../../../../lib/logs/get_logs';
const { advanced: metricSetAdvanced, overview: metricSetOverview } = metricSet;
@ -47,6 +48,7 @@ export function esIndexRoute(server) {
const start = req.payload.timeRange.min;
const end = req.payload.timeRange.max;
const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs);
const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, ccs);
const isAdvanced = req.payload.is_advanced;
const metricSet = isAdvanced ? metricSetAdvanced : metricSetOverview;
@ -57,6 +59,7 @@ export function esIndexRoute(server) {
const indexSummary = await getIndexSummary(req, esIndexPattern, shardStats, { clusterUuid, indexUuid, start, end });
const metrics = await getMetrics(req, esIndexPattern, metricSet, [{ term: { 'index_stats.index': indexUuid } }]);
let logs;
let shardAllocation;
if (!isAdvanced) {
// TODO: Why so many fields needed for a single component (shard legend)?
@ -69,6 +72,8 @@ export function esIndexRoute(server) {
};
const shards = await getShardAllocation(req, esIndexPattern, allocationOptions);
logs = await getLogs(config, req, filebeatIndexPattern, { clusterUuid, indexUuid, start, end });
shardAllocation = {
shards,
shardStats: { nodes: shardStats.nodes },
@ -80,6 +85,7 @@ export function esIndexRoute(server) {
return {
indexSummary,
metrics,
logs,
...shardAllocation,
};

View file

@ -13,7 +13,8 @@ import { getMetrics } from '../../../../lib/details/get_metrics';
import { handleError } from '../../../../lib/errors/handle_error';
import { prefixIndexPattern } from '../../../../lib/ccs_utils';
import { metricSets } from './metric_set_node_detail';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants';
import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_FILEBEAT } from '../../../../../common/constants';
import { getLogs } from '../../../../lib/logs/get_logs';
const { advanced: metricSetAdvanced, overview: metricSetOverview } = metricSets;
@ -47,6 +48,7 @@ export function esNodeRoute(server) {
const start = req.payload.timeRange.min;
const end = req.payload.timeRange.max;
const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs);
const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, '*');
const isAdvanced = req.payload.is_advanced;
let metricSet;
@ -72,6 +74,8 @@ export function esNodeRoute(server) {
const nodeSummary = await getNodeSummary(req, esIndexPattern, clusterState, shardStats, { clusterUuid, nodeUuid, start, end });
const metrics = await getMetrics(req, esIndexPattern, metricSet, [{ term: { 'source_node.uuid': nodeUuid } }]);
let logs;
let shardAllocation;
if (!isAdvanced) {
// TODO: Why so many fields needed for a single component (shard legend)?
@ -90,11 +94,14 @@ export function esNodeRoute(server) {
nodes: shardStats.nodes, // for identifying nodes that shard relocates to
stateUuid, // for debugging/troubleshooting
};
logs = await getLogs(config, req, filebeatIndexPattern, { clusterUuid, nodeUuid, start, end });
}
return {
nodeSummary,
metrics,
logs,
...shardAllocation
};
} catch (err) {

View file

@ -13,7 +13,8 @@ import { getShardStats } from '../../../../lib/elasticsearch/shards';
import { handleError } from '../../../../lib/errors/handle_error';
import { prefixIndexPattern } from '../../../../lib/ccs_utils';
import { metricSet } from './metric_set_overview';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants';
import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_FILEBEAT } from '../../../../../common/constants';
import { getLogs } from '../../../../lib/logs';
export function esOverviewRoute(server) {
server.route({
@ -38,18 +39,24 @@ export function esOverviewRoute(server) {
const ccs = req.payload.ccs;
const clusterUuid = req.params.clusterUuid;
const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs);
const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, '*');
const start = req.payload.timeRange.min;
const end = req.payload.timeRange.max;
try {
const [ clusterStats, metrics, shardActivity ] = await Promise.all([
const [ clusterStats, metrics, shardActivity, logs ] = await Promise.all([
getClusterStats(req, esIndexPattern, clusterUuid),
getMetrics(req, esIndexPattern, metricSet),
getLastRecovery(req, esIndexPattern),
getLogs(config, req, filebeatIndexPattern, { clusterUuid, start, end })
]);
const shardStats = await getShardStats(req, esIndexPattern, clusterStats);
return {
clusterStatus: getClusterStatus(clusterStats, shardStats),
metrics,
logs,
shardActivity,
};
} catch (err) {

View file

@ -59,6 +59,19 @@
}
},
"status": "green"
},
"logs": {
"enabled": false,
"reason": {
"clusterExists": null,
"indexPatternExists": false,
"indexPatternInTimeRangeExists": false,
"nodeExists": null,
"indexExists": null,
"typeExists": false,
"typeExistsAtAnyTime": false
},
"types": []
}
},
"logstash": {

View file

@ -9,6 +9,20 @@
"totalShards": 10,
"status": "green"
},
"logs": {
"enabled": false,
"limit": 10,
"reason": {
"clusterExists": false,
"indexPatternExists": false,
"indexPatternInTimeRangeExists": false,
"typeExistsAtAnyTime": false,
"nodeExists": null,
"indexExists": false,
"typeExists": false
},
"logs": []
},
"metrics": {
"index_search_request_rate": [
{

View file

@ -10,6 +10,20 @@
"status": "Offline",
"isOnline": false
},
"logs": {
"enabled": false,
"limit": 10,
"reason": {
"clusterExists": false,
"indexPatternExists": false,
"indexPatternInTimeRangeExists": false,
"typeExistsAtAnyTime": false,
"nodeExists": false,
"indexExists": null,
"typeExists": false
},
"logs": []
},
"metrics": {
"node_latency": [{
"bucket_size": "10 seconds",

View file

@ -5748,5 +5748,19 @@
"total_time_in_millis": 0
}
}
]
],
"logs": {
"enabled": false,
"limit": 10,
"reason": {
"clusterExists": false,
"indexPatternExists": false,
"indexPatternInTimeRangeExists": false,
"typeExistsAtAnyTime": false,
"nodeExists": null,
"indexExists": null,
"typeExists": false
},
"logs": []
}
}

View file

@ -14,6 +14,20 @@
"7.0.0-alpha1"
]
},
"logs": {
"enabled": false,
"limit": 10,
"reason": {
"clusterExists": false,
"indexPatternExists": false,
"indexPatternInTimeRangeExists": false,
"typeExistsAtAnyTime": false,
"nodeExists": null,
"indexExists": null,
"typeExists": false
},
"logs": []
},
"metrics": {
"cluster_index_latency": [
{

View file

@ -14,6 +14,20 @@
"7.0.0-alpha1"
]
},
"logs": {
"enabled": false,
"limit": 10,
"reason": {
"clusterExists": false,
"indexPatternExists": false,
"indexPatternInTimeRangeExists": false,
"typeExistsAtAnyTime": false,
"nodeExists": null,
"indexExists": null,
"typeExists": false
},
"logs": []
},
"metrics": {
"cluster_index_latency": [
{

View file

@ -15,5 +15,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./logstash'));
loadTestFile(require.resolve('./common'));
loadTestFile(require.resolve('./standalone_cluster'));
loadTestFile(require.resolve('./logs'));
});
}

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import clusterFixture from './fixtures/cluster';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('cluster', () => {
const archive = 'monitoring/logs';
const timeRange = {
min: '2019-03-15T16:19:22.161Z',
max: '2019-03-15T17:19:22.161Z'
};
before('load archive', () => {
return esArchiver.load(archive);
});
after('unload archive', () => {
return esArchiver.unload(archive);
});
it('should get log types at the cluster level', async () => {
const { body } = await supertest
.post('/api/monitoring/v1/clusters/ZR3ZlJLUTV2V_GlplB83jQ')
.set('kbn-xsrf', 'xxx')
.send({ timeRange })
.expect(200);
expect(body[0].elasticsearch.logs).to.eql(clusterFixture);
});
});
}

View file

@ -0,0 +1,4 @@
{
"enabled": true,
"types": [{"type":"server","levels":[{"level":"info","count":38},{"level":"warn","count":1}]},{"type":"deprecation","levels":[{"level":"warn","count":3}]}]
}

View file

@ -0,0 +1,13 @@
{
"enabled": true,
"logs": [{
"timestamp": "2019-03-15T17:07:21.089Z",
"component": "o.e.n.Node",
"node": "Elastic-MBP.local",
"index": ".monitoring-es",
"level": "INFO",
"type": "server",
"message": "started"
}],
"limit": 10
}

View file

@ -0,0 +1,79 @@
{
"enabled": true,
"logs": [{
"timestamp": "2019-03-15T17:19:07.365Z",
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"node": "Elastic-MBP.local",
"level": "WARN",
"type": "deprecation",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead."
}, {
"timestamp": "2019-03-15T17:18:57.366Z",
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"node": "Elastic-MBP.local",
"level": "WARN",
"type": "deprecation",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead."
}, {
"timestamp": "2019-03-15T17:18:47.400Z",
"component": "o.e.c.m.MetaDataCreateIndexService",
"node": "Elastic-MBP.local",
"index": ".monitoring-beats-7-2019.03.15",
"level": "INFO",
"type": "server",
"message": "creating index, cause [auto(bulk api)], templates [.monitoring-beats], shards [1]/[0], mappings [_doc]"
}, {
"timestamp": "2019-03-15T17:18:47.387Z",
"component": "o.e.d.x.m.r.a.RestMonitoringBulkAction",
"node": "Elastic-MBP.local",
"level": "WARN",
"type": "deprecation",
"message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead."
}, {
"timestamp": "2019-03-15T17:18:42.084Z",
"component": "o.e.c.m.MetaDataMappingService",
"node": "Elastic-MBP.local",
"index": "filebeat-8.0.0-2019.03.15-000001",
"level": "INFO",
"type": "server",
"message": "update_mapping [_doc]"
}, {
"timestamp": "2019-03-15T17:18:41.811Z",
"component": "o.e.c.m.MetaDataMappingService",
"node": "Elastic-MBP.local",
"index": "filebeat-8.0.0-2019.03.15-000001",
"level": "INFO",
"type": "server",
"message": "update_mapping [_doc]"
}, {
"timestamp": "2019-03-15T17:18:41.447Z",
"component": "o.e.c.m.MetaDataCreateIndexService",
"node": "Elastic-MBP.local",
"index": "filebeat-8.0.0-2019.03.15-000001",
"level": "INFO",
"type": "server",
"message": "creating index, cause [api], templates [filebeat-8.0.0], shards [1]/[1], mappings [_doc]"
}, {
"timestamp": "2019-03-15T17:18:41.385Z",
"component": "o.e.c.m.MetaDataIndexTemplateService",
"node": "Elastic-MBP.local",
"level": "INFO",
"type": "server",
"message": "adding template [filebeat-8.0.0] for index patterns [filebeat-8.0.0-*]"
}, {
"timestamp": "2019-03-15T17:18:41.185Z",
"component": "o.e.x.i.a.TransportPutLifecycleAction",
"node": "Elastic-MBP.local",
"level": "INFO",
"type": "server",
"message": "adding index lifecycle policy [filebeat-8.0.0]"
}, {
"timestamp": "2019-03-15T17:18:36.137Z",
"component": "o.e.c.r.a.AllocationService",
"node": "Elastic-MBP.local",
"level": "INFO",
"type": "server",
"message": "Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-7-2019.03.15][0]] ...])."
}],
"limit": 10
}

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 default function ({ loadTestFile }) {
describe('Logs', () => {
loadTestFile(require.resolve('./node_detail'));
loadTestFile(require.resolve('./index_detail'));
loadTestFile(require.resolve('./cluster'));
});
}

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import indexDetailFixture from './fixtures/index_detail';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('cluster', () => {
const archive = 'monitoring/logs';
const timeRange = {
min: '2019-03-15T16:19:22.161Z',
max: '2019-03-15T17:19:22.161Z'
};
before('load archive', () => {
return esArchiver.load(archive);
});
after('unload archive', () => {
return esArchiver.unload(archive);
});
it('should get logs for the specific index', async () => {
const { body } = await supertest
.post('/api/monitoring/v1/clusters/ZR3ZlJLUTV2V_GlplB83jQ/elasticsearch/indices/.monitoring-es')
.set('kbn-xsrf', 'xxx')
.send({ timeRange, is_advanced: false })
.expect(200);
expect(body.logs).to.eql(indexDetailFixture);
});
});
}

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import nodeDetailFixture from './fixtures/node_detail';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('cluster', () => {
const archive = 'monitoring/logs';
const timeRange = {
min: '2019-03-15T16:19:22.161Z',
max: '2019-03-15T17:19:22.161Z'
};
before('load archive', () => {
return esArchiver.load(archive);
});
after('unload archive', () => {
return esArchiver.unload(archive);
});
it('should get logs for the specific node', async () => {
const { body } = await supertest
.post('/api/monitoring/v1/clusters/ZR3ZlJLUTV2V_GlplB83jQ/elasticsearch/nodes/-pH5RhfsRl6FDeTPwD5vEw')
.set('kbn-xsrf', 'xxx')
.send({ timeRange, is_advanced: false })
.expect(200);
expect(body.logs).to.eql(nodeDetailFixture);
});
});
}

View file

@ -1 +1,51 @@
[{"cluster_uuid":"__standalone_cluster__","license":{},"elasticsearch":{"cluster_stats":{"indices":{},"nodes":{"count":{},"jvm":{}}}},"logstash":{},"kibana":{},"beats":{"totalEvents":348,"bytesSent":319913,"beats":{"total":1,"types":[{"type":"Packetbeat","count":1}]}},"apm":{"totalEvents":0,"memRss":0,"memTotal":0,"apms":{"total":0}},"alerts":{"message":"Cluster Alerts are not displayed because the [production] cluster's license could not be determined."},"isPrimary":false}]
[{
"cluster_uuid": "__standalone_cluster__",
"license": {},
"elasticsearch": {
"cluster_stats": {
"indices": {},
"nodes": {
"count": {},
"jvm": {}
}
},
"logs": {
"enabled": false,
"reason": {
"clusterExists": null,
"indexPatternExists": false,
"indexPatternInTimeRangeExists": false,
"typeExistsAtAnyTime": false,
"nodeExists": null,
"indexExists": null,
"typeExists": false
},
"types": []
}
},
"logstash": {},
"kibana": {},
"beats": {
"totalEvents": 348,
"bytesSent": 319913,
"beats": {
"total": 1,
"types": [{
"type": "Packetbeat",
"count": 1
}]
}
},
"apm": {
"totalEvents": 0,
"memRss": 0,
"memTotal": 0,
"apms": {
"total": 0
}
},
"alerts": {
"message": "Cluster Alerts are not displayed because the [production] cluster's license could not be determined."
},
"isPrimary": false
}]

File diff suppressed because it is too large Load diff