[Monitoring] CCR UI (#23013)

* Initial version of CCR monitoring UI

* Adding missing files

* Use icons

* Use new column header text

* Update tests

* Basic of shard detail page

* Do these in parallel

* Disable time picker on ccr page

* Remove summary for now

* Remove unnecessary code here

* Fix a few things on the shard page

* Only send down what we need

* update snapshot

* Handle no ccr_stats documents

* Ensure we fetch the latest

* Updates

* Format the time

* Add api integration tests

* Adding pagination and sorting

* Updated query logic

* Change this back

* Add specific information about the follower and leader lag ops

* Update tests

* UI updates

* Address PR issues

* Fix tests

* Update shapshots

* Add timestamp

* Update tests

* Add a few snapshot tests

* Use timezone formatter

* Fix tests

* Fix aligment of shard table

* PR feedback

* Update snapshots

* Update snapshot
This commit is contained in:
Chris Roberson 2018-10-05 08:46:21 -04:00 committed by GitHub
parent a648d0bff3
commit 2caf6ecb4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2856 additions and 3 deletions

View file

@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Ccr that it renders normally 1`] = `
<EuiPage
restrictWidth={false}
>
<EuiPageBody
restrictWidth={false}
>
<EuiPageContent
panelPaddingSize="l"
>
<EuiPageContentBody>
<EuiInMemoryTable
className="monitoringElasticsearchCcrListingTable"
columns={
Array [
Object {
"field": "index",
"name": "Index",
"render": [Function],
"sortable": true,
},
Object {
"field": "follows",
"name": "Follows",
"sortable": true,
},
Object {
"field": "opsSynced",
"name": "Ops synced",
"sortable": true,
},
Object {
"field": "syncLagTime",
"name": "Last fetch time",
"render": [Function],
"sortable": true,
},
Object {
"field": "syncLagOps",
"name": "Sync Lag (ops)",
"sortable": true,
},
Object {
"field": "error",
"name": "Error",
"render": [Function],
"sortable": true,
},
]
}
itemId="id"
itemIdToExpandedRowMap={Object {}}
items={
Array [
Object {
"follows": "leader",
"id": "follower",
"index": "follower",
"opsSynced": 400,
"shards": Array [
Object {
"opsSynced": 200,
"shardId": 0,
"syncLagOps": 2,
"syncLagOpsFollower": 1,
"syncLagOpsLeader": 1,
"syncLagTime": 45000,
},
Object {
"opsSynced": 200,
"shardId": 1,
"syncLagOps": 1,
"syncLagOpsFollower": 0,
"syncLagOpsLeader": 1,
"syncLagTime": 60000,
},
],
"syncLagOps": 5,
"syncLagTime": 60000,
},
Object {
"error": "not_working_properly",
"follows": "leader2",
"id": "follower2",
"index": "follower2",
"opsSynced": 50,
"shards": Array [
Object {
"opsSynced": 20,
"shardId": 1,
"syncLagOps": 0,
"syncLagOpsFollower": 0,
"syncLagOpsLeader": 0,
"syncLagTime": 11000,
},
Object {
"error": "not_working_properly",
"opsSynced": 30,
"shardId": 2,
"syncLagOps": 5,
"syncLagOpsFollower": 5,
"syncLagOpsLeader": 0,
"syncLagTime": 1000,
},
],
"syncLagOps": 1,
"syncLagTime": 12000,
},
]
}
pagination={false}
responsive={true}
sorting={
Object {
"sort": Object {
"direction": "asc",
"field": "index",
},
}
}
/>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
`;

View file

@ -0,0 +1,7 @@
/**
* [1] - We want the collapsed table (that shows the shard data) to be inline
* with the columns from the main table so we need to remove the padding
*/
.monitoringElasticsearchCcrListingTable .euiTableRow-isExpandedRow > .euiTableRowCell > .euiTableCellContent {
padding: 0; /* [1] */
}

View file

@ -0,0 +1,211 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, Component } from 'react';
import {
EuiInMemoryTable,
EuiLink,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiIcon,
EuiIconTip,
EuiTextColor
} from '@elastic/eui';
import './ccr.css';
function toSeconds(ms) {
return Math.floor(ms / 1000) + 's';
}
export class Ccr extends Component {
constructor(props) {
super(props);
this.state = {
itemIdToExpandedRowMap: {},
};
}
toggleShards(index, shards) {
const itemIdToExpandedRowMap = {
...this.state.itemIdToExpandedRowMap
};
if (itemIdToExpandedRowMap[index]) {
delete itemIdToExpandedRowMap[index];
} else {
let pagination = {
initialPageSize: 5,
pageSizeOptions: [5, 10, 20]
};
if (shards.length <= pagination.initialPageSize) {
pagination = false;
}
itemIdToExpandedRowMap[index] = (
<EuiInMemoryTable
items={shards}
columns={[
{
field: 'shardId',
name: 'Shard',
render: shardId => {
return (
<EuiLink href={`#/elasticsearch/ccr/${index}/shard/${shardId}`}>
{shardId}
</EuiLink>
);
}
},
{
render: () => null
},
{
field: 'opsSynced',
name: 'Ops synced'
},
{
field: 'syncLagTime',
name: 'Last fetch time',
render: syncLagTime => <span>{toSeconds(syncLagTime)}</span>
},
{
field: 'syncLagOps',
name: 'Sync Lag (ops)',
render: (syncLagOps, data) => (
<span>
{syncLagOps}
&nbsp;&nbsp;
<EuiIconTip
size="m"
type="iInCircle"
content={(
<Fragment>
<span>Leader lag: {data.syncLagOpsLeader}</span>
<br/>
<span>Follower lag: {data.syncLagOpsFollower}</span>
</Fragment>
)}
position="right"
/>
</span>
)
},
{
field: 'error',
name: 'Error',
render: error => (
<EuiTextColor color="danger">
{error}
</EuiTextColor>
)
}
]}
sorting={true}
pagination={pagination}
/>
);
}
this.setState({ itemIdToExpandedRowMap });
}
renderTable() {
const { data } = this.props;
const items = data;
let pagination = {
initialPageSize: 5,
pageSizeOptions: [5, 10, 20]
};
if (items.length <= pagination.initialPageSize) {
pagination = false;
}
const sorting = {
sort: {
field: 'index',
direction: 'asc',
},
};
return (
<EuiInMemoryTable
className="monitoringElasticsearchCcrListingTable"
columns={[
{
field: 'index',
name: 'Index',
sortable: true,
render: (index, { shards }) => {
const expanded = !!this.state.itemIdToExpandedRowMap[index];
return (
<EuiLink onClick={() => this.toggleShards(index, shards)}>
{index}
&nbsp;
{ expanded ? <EuiIcon type="arrowUp" /> : <EuiIcon type="arrowDown" /> }
</EuiLink>
);
}
},
{
field: 'follows',
sortable: true,
name: 'Follows'
},
{
field: 'opsSynced',
sortable: true,
name: 'Ops synced'
},
{
field: 'syncLagTime',
sortable: true,
name: 'Last fetch time',
render: syncLagTime => <span>{toSeconds(syncLagTime)}</span>
},
{
field: 'syncLagOps',
sortable: true,
name: 'Sync Lag (ops)',
},
{
field: 'error',
sortable: true,
name: 'Error',
render: error => (
<EuiTextColor color="danger">
{error}
</EuiTextColor>
)
}
]}
items={items}
pagination={pagination}
sorting={sorting}
itemId="id"
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
/>
);
}
render() {
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent>
<EuiPageContentBody>
{this.renderTable()}
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}

View file

@ -0,0 +1,73 @@
/*
* 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 { Ccr } from './ccr';
describe('Ccr', () => {
test('that it renders normally', () => {
const data = [
{
follows: 'leader',
id: 'follower',
index: 'follower',
opsSynced: 400,
syncLagOps: 5,
syncLagTime: 60000,
shards: [
{
opsSynced: 200,
shardId: 0,
syncLagOps: 2,
syncLagOpsFollower: 1,
syncLagOpsLeader: 1,
syncLagTime: 45000
},
{
opsSynced: 200,
shardId: 1,
syncLagOps: 1,
syncLagOpsFollower: 0,
syncLagOpsLeader: 1,
syncLagTime: 60000
}
]
},
{
follows: 'leader2',
id: 'follower2',
index: 'follower2',
opsSynced: 50,
syncLagOps: 1,
syncLagTime: 12000,
error: 'not_working_properly',
shards: [
{
opsSynced: 20,
shardId: 1,
syncLagOps: 0,
syncLagOpsFollower: 0,
syncLagOpsLeader: 0,
syncLagTime: 11000
},
{
opsSynced: 30,
shardId: 2,
syncLagOps: 5,
syncLagOpsFollower: 5,
syncLagOpsLeader: 0,
syncLagTime: 1000,
error: 'not_working_properly'
}
]
}
];
const component = shallow(<Ccr data={data} />);
expect(component).toMatchSnapshot();
});
});

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 { Ccr } from './ccr';

View file

@ -0,0 +1,215 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CcrShard that is renders an exception properly 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="m"
>
<EuiTitle
color="danger"
size="s"
>
<h3>
<EuiTextColor
color="danger"
component="span"
>
Errors
</EuiTextColor>
</h3>
</EuiTitle>
<EuiSpacer
size="s"
/>
<EuiBasicTable
columns={
Array [
Object {
"field": "exception.type",
"name": "Type",
},
Object {
"field": "exception.reason",
"name": "Reason",
"width": "75%",
},
]
}
items={
Array [
Object {
"reason": "not sure but something happened",
"type": "something_is_wrong",
},
]
}
noItemsMessage="No items found"
responsive={true}
/>
</EuiPanel>
`;
exports[`CcrShard that it renders normally 1`] = `
<EuiPage
restrictWidth={false}
style={
Object {
"backgroundColor": "white",
}
}
>
<EuiPageBody
restrictWidth={false}
>
<Status
formattedLeader="leader on remote"
oldestStat={
Object {
"number_of_failed_fetches": 0,
"number_of_operations_indexed": 2976,
}
}
stat={
Object {
"fetch_exceptions": Array [],
"follower_global_checkpoint": 3049,
"follower_index": "follower",
"follower_max_seq_no": 3049,
"last_requested_seq_no": 3049,
"leader_global_checkpoint": 3049,
"leader_index": "leader",
"leader_max_seq_no": 3049,
"mapping_version": 2,
"number_of_concurrent_reads": 1,
"number_of_concurrent_writes": 0,
"number_of_failed_bulk_operations": 0,
"number_of_failed_fetches": 0,
"number_of_operations_indexed": 3050,
"number_of_queued_writes": 0,
"number_of_successful_bulk_operations": 3050,
"number_of_successful_fetches": 3050,
"operations_received": 3050,
"shard_id": 0,
"time_since_last_fetch_millis": 9402,
"total_fetch_time_millis": 44128980,
"total_index_time_millis": 41827,
"total_transferred_bytes": 234156,
}
}
/>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={true}
>
<React.Fragment>
<EuiFlexItem
component="div"
grow={true}
key="0"
style={
Object {
"minWidth": "45%",
}
}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="m"
>
<MonitoringTimeseriesContainer />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
key="1"
style={
Object {
"minWidth": "45%",
}
}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="m"
>
<MonitoringTimeseriesContainer />
</EuiPanel>
</EuiFlexItem>
</React.Fragment>
</EuiFlexGroup>
<EuiHorizontalRule
margin="l"
size="full"
/>
<EuiAccordion
buttonContent={
<EuiTitle
size="m"
>
<h2>
Advanced
</h2>
</EuiTitle>
}
id="ccrLatestStat"
initialIsOpen={false}
paddingSize="l"
>
<React.Fragment>
<EuiTitle
size="s"
>
<h4>
September 27, 2018 9:32:09 AM
</h4>
</EuiTitle>
<EuiHorizontalRule
margin="l"
size="full"
/>
<EuiCodeBlock
language="json"
>
{
"fetch_exceptions": [],
"follower_global_checkpoint": 3049,
"follower_index": "follower",
"follower_max_seq_no": 3049,
"last_requested_seq_no": 3049,
"leader_global_checkpoint": 3049,
"leader_index": "leader",
"leader_max_seq_no": 3049,
"mapping_version": 2,
"number_of_concurrent_reads": 1,
"number_of_concurrent_writes": 0,
"number_of_failed_bulk_operations": 0,
"number_of_failed_fetches": 0,
"number_of_operations_indexed": 3050,
"number_of_queued_writes": 0,
"number_of_successful_bulk_operations": 3050,
"number_of_successful_fetches": 3050,
"operations_received": 3050,
"shard_id": 0,
"time_since_last_fetch_millis": 9402,
"total_fetch_time_millis": 44128980,
"total_index_time_millis": 41827,
"total_transferred_bytes": 234156
}
</EuiCodeBlock>
</React.Fragment>
</EuiAccordion>
</EuiPageBody>
</EuiPage>
`;

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, { Fragment, PureComponent } from 'react';
import {
EuiPage,
EuiPageBody,
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
EuiBasicTable,
EuiCodeBlock,
EuiTextColor,
EuiHorizontalRule,
EuiAccordion,
} from '@elastic/eui';
import { MonitoringTimeseriesContainer } from '../../chart';
import { Status } from './status';
import { formatDateTimeLocal } from '../../../../common/formatting';
export class CcrShard extends PureComponent {
renderCharts() {
const { metrics } = this.props;
const seriesToShow = [
metrics.ccr_sync_lag_time,
metrics.ccr_sync_lag_ops
];
const charts = seriesToShow.map((data, index) => (
<EuiFlexItem style={{ minWidth: '45%' }} key={index}>
<EuiPanel>
<MonitoringTimeseriesContainer
series={data}
/>
</EuiPanel>
</EuiFlexItem>
));
return (
<Fragment>
{charts}
</Fragment>
);
}
renderErrors() {
const { stat } = this.props;
if (stat.fetch_exceptions && stat.fetch_exceptions.length > 0) {
return (
<Fragment>
<EuiPanel>
<EuiTitle size="s" color="danger">
<h3>
<EuiTextColor color="danger">Errors</EuiTextColor>
</h3>
</EuiTitle>
<EuiSpacer size="s"/>
<EuiBasicTable
items={stat.fetch_exceptions}
columns={[
{
name: 'Type',
field: 'exception.type'
},
{
name: 'Reason',
field: 'exception.reason',
width: '75%'
}
]}
/>
</EuiPanel>
<EuiHorizontalRule/>
</Fragment>
);
}
return null;
}
renderLatestStat() {
const { stat, timestamp } = this.props;
return (
<EuiAccordion
id="ccrLatestStat"
buttonContent={<EuiTitle><h2>Advanced</h2></EuiTitle>}
paddingSize="l"
>
<Fragment>
<EuiTitle size="s">
<h4>{formatDateTimeLocal(timestamp)}</h4>
</EuiTitle>
<EuiHorizontalRule/>
<EuiCodeBlock language="json">
{JSON.stringify(stat, null, 2)}
</EuiCodeBlock>
</Fragment>
</EuiAccordion>
);
}
render() {
const { stat, oldestStat, formattedLeader } = this.props;
return (
<EuiPage style={{ backgroundColor: 'white' }}>
<EuiPageBody>
<Status stat={stat} formattedLeader={formattedLeader} oldestStat={oldestStat}/>
<EuiSpacer size="s"/>
{this.renderErrors()}
<EuiFlexGroup wrap>
{this.renderCharts()}
</EuiFlexGroup>
<EuiHorizontalRule/>
{this.renderLatestStat()}
</EuiPageBody>
</EuiPage>
);
}
}

View file

@ -0,0 +1,69 @@
/*
* 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 { CcrShard } from './ccr_shard';
describe('CcrShard', () => {
const props = {
formattedLeader: 'leader on remote',
metrics: [],
stat: {
fetch_exceptions: [],
follower_global_checkpoint: 3049,
follower_index: 'follower',
follower_max_seq_no: 3049,
last_requested_seq_no: 3049,
leader_global_checkpoint: 3049,
leader_index: 'leader',
leader_max_seq_no: 3049,
mapping_version: 2,
number_of_concurrent_reads: 1,
number_of_concurrent_writes: 0,
number_of_failed_bulk_operations: 0,
number_of_failed_fetches: 0,
number_of_operations_indexed: 3050,
number_of_queued_writes: 0,
number_of_successful_bulk_operations: 3050,
number_of_successful_fetches: 3050,
operations_received: 3050,
shard_id: 0,
time_since_last_fetch_millis: 9402,
total_fetch_time_millis: 44128980,
total_index_time_millis: 41827,
total_transferred_bytes: 234156,
},
oldestStat: {
number_of_failed_fetches: 0,
number_of_operations_indexed: 2976
},
timestamp: '2018-09-27T13:32:09.412Z'
};
test('that it renders normally', () => {
const component = shallow(<CcrShard {...props} />);
expect(component).toMatchSnapshot();
});
test('that is renders an exception properly', () => {
const localProps = {
...props,
stat: {
...props.stat,
fetch_exceptions: [
{
type: 'something_is_wrong',
reason: 'not sure but something happened'
}
]
}
};
const component = shallow(<CcrShard {...localProps} />);
expect(component.find('EuiPanel').get(0)).toMatchSnapshot();
});
});

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 { CcrShard } from './ccr_shard';

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { SummaryStatus } from '../../summary_status';
import { formatMetric } from '../../../lib/format_number';
export function Status({ stat, formattedLeader, oldestStat }) {
const {
follower_index: followerIndex,
shard_id: shardId,
number_of_operations_indexed: operationsReceived,
number_of_failed_fetches: failedFetches
} = stat;
const {
number_of_operations_indexed: oldestOperationsReceived,
number_of_failed_fetches: oldestFailedFetches
} = oldestStat;
const metrics = [
{
label: 'Follower Index',
value: followerIndex,
dataTestSubj: 'followerIndex'
},
{
label: 'Shard Id',
value: shardId,
dataTestSubj: 'shardId'
},
{
label: 'Leader Index',
value: formattedLeader,
dataTestSubj: 'leaderIndex'
},
{
label: 'Ops Synced',
value: formatMetric(operationsReceived - oldestOperationsReceived, 'int_commas'),
dataTestSubj: 'operationsReceived'
},
{
label: 'Failed Fetches',
value: formatMetric(failedFetches - oldestFailedFetches, 'int_commas'),
dataTestSubj: 'failedFetches'
},
];
return (
<SummaryStatus
metrics={metrics}
data-test-subj="ccrDetailStatus"
/>
);
}

View file

@ -35,6 +35,7 @@
<a ng-if="!monitoringMain.instance" kbn-href="#/elasticsearch/nodes" class="kuiLocalTab" ng-class="{'kuiLocalTab-isSelected': monitoringMain.isActiveTab('nodes')}">Nodes</a>
<a ng-if="!monitoringMain.instance" kbn-href="#/elasticsearch/indices" class="kuiLocalTab" ng-class="{'kuiLocalTab-isSelected': monitoringMain.isActiveTab('indices')}">Indices</a>
<a ng-if="!monitoringMain.instance && monitoringMain.isMlSupported()" kbn-href="#/elasticsearch/ml_jobs" class="kuiLocalTab" ng-class="{'kuiLocalTab-isSelected': monitoringMain.isActiveTab('ml')}">Jobs</a>
<a ng-if="!monitoringMain.instance" kbn-href="#/elasticsearch/ccr" class="kuiLocalTab" ng-class="{'kuiLocalTab-isSelected': monitoringMain.isActiveTab('ccr')}">CCR</a>
<a
ng-if="monitoringMain.instance && (monitoringMain.name === 'nodes' || monitoringMain.name === 'indices')"
kbn-href="#/elasticsearch/{{ monitoringMain.name }}/{{ monitoringMain.resolver }}"

View file

@ -27,6 +27,8 @@ function getElasticsearchBreadcrumbs(mainInstance) {
} else if (mainInstance.name === 'ml') {
// ML Instance (for user later)
breadcrumbs.push(createCrumb('#/elasticsearch/ml_jobs', 'Jobs'));
} else if (mainInstance.name === 'ccr_shard') {
breadcrumbs.push(createCrumb('#/elasticsearch/ccr', 'CCR'));
}
breadcrumbs.push(createCrumb(null, mainInstance.instance));
} else {

View file

@ -17,6 +17,8 @@ import './elasticsearch/index/advanced';
import './elasticsearch/nodes';
import './elasticsearch/node';
import './elasticsearch/node/advanced';
import './elasticsearch/ccr';
import './elasticsearch/ccr/shard';
import './elasticsearch/ml_jobs';
import './kibana/overview';
import './kibana/instances';

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler';
import { timefilter } from 'ui/timefilter';
export function getPageData($injector) {
const $http = $injector.get('$http');
const globalState = $injector.get('globalState');
const timeBounds = timefilter.getBounds();
const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr`;
return $http.post(url, {
ccs: globalState.ccs,
timeRange: {
min: timeBounds.min.toISOString(),
max: timeBounds.max.toISOString()
}
})
.then(response => response.data)
.catch((err) => {
const Private = $injector.get('Private');
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);
});
}

View file

@ -0,0 +1,7 @@
<monitoring-main
product="elasticsearch"
name="ccr"
data-test-subj="elasticsearchCcrListingPage"
>
<div id="elasticsearchCcrReact"></div>
</monitoring-main>

View file

@ -0,0 +1,41 @@
/*
* 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 uiRoutes from 'ui/routes';
import { getPageData } from './get_page_data';
import template from './index.html';
import { Ccr } from '../../../components/elasticsearch/ccr';
import { MonitoringViewBaseController } from '../../base_controller';
uiRoutes.when('/elasticsearch/ccr', {
template,
resolve: {
pageData: getPageData,
},
controllerAs: 'elasticsearchCcr',
controller: class ElasticsearchCcrController extends MonitoringViewBaseController {
constructor($injector, $scope) {
super({
title: 'Elasticsearch - Ccr',
reactNodeId: 'elasticsearchCcrReact',
getPageData,
$scope,
$injector
});
$scope.$watch(() => this.data, data => {
this.renderReact(data);
});
this.renderReact = ({ data }) => {
super.renderReact(
<Ccr data={data} />
);
};
}
}
});

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler';
import { timefilter } from 'ui/timefilter';
export function getPageData($injector) {
const $http = $injector.get('$http');
const $route = $injector.get('$route');
const globalState = $injector.get('globalState');
const timeBounds = timefilter.getBounds();
const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr/${$route.current.params.index}/shard/${$route.current.params.shardId}`; // eslint-disable-line max-len
return $http.post(url, {
ccs: globalState.ccs,
timeRange: {
min: timeBounds.min.toISOString(),
max: timeBounds.max.toISOString()
}
})
.then(response => response.data)
.catch((err) => {
const Private = $injector.get('Private');
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);
});
}

View file

@ -0,0 +1,8 @@
<monitoring-main
product="elasticsearch"
name="ccr_shard"
instance="{{ instance }}"
data-test-subj="elasticsearchCcrShardPage"
>
<div id="elasticsearchCcrShardReact"></div>
</monitoring-main>

View file

@ -0,0 +1,49 @@
/*
* 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 { get } from 'lodash';
import uiRoutes from 'ui/routes';
import { getPageData } from './get_page_data';
import { routeInitProvider } from 'plugins/monitoring/lib/route_init';
import template from './index.html';
import { MonitoringViewBaseController } from '../../../base_controller';
import { CcrShard } from '../../../../components/elasticsearch/ccr_shard';
uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', {
template,
resolve: {
clusters: function (Private) {
const routeInit = Private(routeInitProvider);
return routeInit();
},
pageData: getPageData,
},
controllerAs: 'elasticsearchCcr',
controller: class ElasticsearchCcrController extends MonitoringViewBaseController {
constructor($injector, $scope, pageData) {
super({
title: 'Elasticsearch - Ccr - Shard',
reactNodeId: 'elasticsearchCcrShardReact',
getPageData,
$scope,
$injector
});
$scope.instance = `Index: ${get(pageData, 'stat.follower_index')} Shard: ${get(pageData, 'stat.shard_id')}`;
$scope.$watch(() => this.data, data => {
this.renderReact(data);
});
this.renderReact = (props) => {
super.renderReact(
<CcrShard {...props} />
);
};
}
}
});

View file

@ -184,6 +184,7 @@ function handleSeries(metric, min, max, bucketSizeInSeconds, response) {
const lastUsableBucketIndex = findLastUsableBucketIndex(buckets, max, firstUsableBucketIndex, bucketSizeInSeconds * 1000);
let data = [];
if (firstUsableBucketIndex <= lastUsableBucketIndex) {
// map buckets to values for charts
const key = derivative ? 'metric_deriv.normalized_value' : 'metric.value';

View file

@ -1777,6 +1777,48 @@ Object {
"units": "",
"uuidField": "beats_stats.beat.uuid",
},
"ccr_sync_lag_ops": DifferenceMetric {
"aggs": Object {
"metric2_max": Object {
"max": Object {
"field": "ccr_stats.follower_global_checkpoint",
},
},
"metric_max": Object {
"max": Object {
"field": "ccr_stats.leader_max_seq_no",
},
},
},
"app": "elasticsearch",
"calculation": [Function],
"derivative": false,
"description": "The number of operations the follower index is lagging behind the leader.",
"field": "",
"format": "0,0.[00]",
"label": "Ops delay",
"metricAgg": "sum",
"timestampField": "timestamp",
"title": "Ops delay",
"type": "ccr",
"units": "",
"uuidField": "source_node.uuid",
},
"ccr_sync_lag_time": MillisecondsToSecondsMetric {
"app": "elasticsearch",
"calculation": [Function],
"derivative": false,
"description": "The amount of time the follower index is lagging behind the leader.",
"field": "ccr_stats.time_since_last_fetch_millis",
"format": "0.[00]",
"label": "Fetch delay",
"metricAgg": "max",
"timestampField": "timestamp",
"title": "Fetch delay",
"type": "ccr",
"units": "s",
"uuidField": "source_node.uuid",
},
"cluster_index_latency": LatencyMetric {
"aggs": Object {
"event_time_in_millis": Object {

View file

@ -33,6 +33,35 @@ export class ElasticsearchMetric extends Metric {
}
}
export class DifferenceMetric extends ElasticsearchMetric {
constructor({ fieldSource, metric, metric2, ...opts }) {
super({
...opts,
field: '', // NOTE: this is not used for this
format: LARGE_FLOAT,
metricAgg: 'sum', // NOTE: this is used for a pointless aggregation
});
this.checkRequiredParams({
metric,
metric2
});
this.aggs = {
metric_max: {
max: { field: `${fieldSource}.${metric}` }
},
metric2_max: {
max: { field: `${fieldSource}.${metric2}` }
},
};
this.calculation = (bucket) => {
return _.get(bucket, 'metric_max.value') - _.get(bucket, 'metric2_max.value');
};
}
}
export class LatencyMetric extends ElasticsearchMetric {
constructor({ metric, fieldSource, ...opts }) {
super({
@ -293,3 +322,16 @@ export class WriteThreadPoolRejectedMetric extends ElasticsearchMetric {
};
}
}
export class MillisecondsToSecondsMetric extends ElasticsearchMetric {
constructor(opts) {
super({
...opts,
units: 's',
});
this.calculation = bucket => {
return _.get(bucket, 'metric.value') / 1000;
};
}
}

View file

@ -15,7 +15,9 @@ import {
ThreadPoolQueueMetric,
ThreadPoolRejectedMetric,
WriteThreadPoolQueueMetric,
WriteThreadPoolRejectedMetric
WriteThreadPoolRejectedMetric,
DifferenceMetric,
MillisecondsToSecondsMetric,
} from './classes';
import {
LARGE_FLOAT,
@ -944,5 +946,29 @@ export const metrics = {
units: '',
type: 'index',
derivative: true
})
}),
// CCR
ccr_sync_lag_time: new MillisecondsToSecondsMetric({
title: 'Fetch delay', // title to use for the chart
type: 'ccr',
field: 'ccr_stats.time_since_last_fetch_millis',
label: 'Fetch delay',
description: 'The amount of time the follower index is lagging behind the leader.',
format: SMALL_FLOAT,
metricAgg: 'max',
units: 'ms'
}),
ccr_sync_lag_ops: new DifferenceMetric({
title: 'Ops delay', // title to use for the chart
type: 'ccr',
fieldSource: 'ccr_stats',
metric: 'leader_max_seq_no',
metric2: 'follower_global_checkpoint',
label: 'Ops delay',
description: 'The number of operations the follower index is lagging behind the leader.',
format: SMALL_FLOAT,
metricAgg: 'max',
units: ''
}),
};

View file

@ -0,0 +1,258 @@
/*
* 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 moment from 'moment';
import { get, groupBy } from 'lodash';
import { handleError } from '../../../../lib/errors/handle_error';
import { prefixIndexPattern } from '../../../../lib/ccs_utils';
function getBucketScript(max, min) {
return {
bucket_script: {
buckets_path: {
max,
min,
},
script: 'params.max - params.min'
}
};
}
function buildRequest(req, config, esIndexPattern) {
const min = moment.utc(req.payload.timeRange.min).valueOf();
const max = moment.utc(req.payload.timeRange.max).valueOf();
const maxBucketSize = config.get('xpack.monitoring.max_bucket_size');
const aggs = {
ops_synced_max: {
max: {
field: 'ccr_stats.number_of_operations_indexed'
}
},
ops_synced_min: {
min: {
field: 'ccr_stats.number_of_operations_indexed'
}
},
last_fetch_time_max: {
max: {
field: 'ccr_stats.time_since_last_fetch_millis'
}
},
last_fetch_time_min: {
min: {
field: 'ccr_stats.time_since_last_fetch_millis'
}
},
lag_ops_leader_max: {
max: {
field: 'ccr_stats.leader_max_seq_no'
}
},
lag_ops_leader_min: {
min: {
field: 'ccr_stats.leader_max_seq_no'
}
},
lag_ops_global_max: {
max: {
field: 'ccr_stats.follower_global_checkpoint'
}
},
lag_ops_global_min: {
min: {
field: 'ccr_stats.follower_global_checkpoint'
}
},
leader_lag_ops_checkpoint_max: {
max: {
field: 'ccr_stats.leader_global_checkpoint'
}
},
leader_lag_ops_checkpoint_min: {
min: {
field: 'ccr_stats.leader_global_checkpoint'
}
},
last_fetch_time: getBucketScript('last_fetch_time_max', 'last_fetch_time_min'),
ops_synced: getBucketScript('ops_synced_max', 'ops_synced_min'),
lag_ops_leader: getBucketScript('lag_ops_leader_max', 'lag_ops_leader_min'),
lag_ops_global: getBucketScript('lag_ops_global_max', 'lag_ops_global_min'),
lag_ops: getBucketScript('lag_ops_leader', 'lag_ops_global'),
lag_ops_leader_checkpoint: getBucketScript('leader_lag_ops_checkpoint_max', 'leader_lag_ops_checkpoint_min'),
leader_lag_ops: getBucketScript('lag_ops_leader', 'lag_ops_leader_checkpoint'),
follower_lag_ops: getBucketScript('lag_ops_leader_checkpoint', 'lag_ops_global'),
};
return {
index: esIndexPattern,
size: maxBucketSize,
filterPath: [
'hits.hits.inner_hits.by_shard.hits.hits._source.ccr_stats.fetch_exceptions',
'hits.hits.inner_hits.by_shard.hits.hits._source.ccr_stats.follower_index',
'hits.hits.inner_hits.by_shard.hits.hits._source.ccr_stats.shard_id',
'aggregations.by_follower_index.buckets.key',
'aggregations.by_follower_index.buckets.leader_index.buckets.key',
'aggregations.by_follower_index.buckets.by_shard_id.buckets.key',
'aggregations.by_follower_index.buckets.by_shard_id.buckets.last_fetch_time.value',
'aggregations.by_follower_index.buckets.by_shard_id.buckets.ops_synced.value',
'aggregations.by_follower_index.buckets.by_shard_id.buckets.lag_ops.value',
'aggregations.by_follower_index.buckets.by_shard_id.buckets.leader_lag_ops.value',
'aggregations.by_follower_index.buckets.by_shard_id.buckets.follower_lag_ops.value',
],
body: {
sort: [{ timestamp: { order: 'desc' } }],
query: {
bool: {
must: [
{
term: {
type: {
value: 'ccr_stats'
}
}
},
{
range: {
timestamp: {
format: 'epoch_millis',
gte: min,
lte: max,
}
}
}
]
}
},
collapse: {
field: 'ccr_stats.follower_index',
inner_hits: {
name: 'by_shard',
sort: [{ timestamp: 'desc' }],
size: maxBucketSize,
collapse: {
field: 'ccr_stats.shard_id',
}
}
},
aggs: {
by_follower_index: {
terms: {
field: 'ccr_stats.follower_index',
size: maxBucketSize,
},
aggs: {
leader_index: {
terms: {
field: 'ccr_stats.leader_index',
size: 1
}
},
by_shard_id: {
terms: {
field: 'ccr_stats.shard_id',
size: 10
},
aggs,
}
}
}
}
}
};
}
export function ccrRoute(server) {
server.route({
method: 'POST',
path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr',
config: {
validate: {
params: Joi.object({
clusterUuid: Joi.string().required()
}),
payload: Joi.object({
ccs: Joi.string().optional(),
timeRange: Joi.object({
min: Joi.date().required(),
max: Joi.date().required()
}).required()
})
}
},
async handler(req, reply) {
const config = server.config();
const ccs = req.payload.ccs;
const esIndexPattern = prefixIndexPattern(config, 'xpack.monitoring.elasticsearch.index_pattern', ccs);
try {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const response = await callWithRequest(req, 'search', buildRequest(req, config, esIndexPattern));
if (!response || Object.keys(response).length === 0) {
reply({ data: [] });
return;
}
const fullStats = get(response, 'hits.hits').reduce((accum, hit) => {
const innerHits = get(hit, 'inner_hits.by_shard.hits.hits');
const innerHitsSource = innerHits.map(innerHit => get(innerHit, '_source.ccr_stats'));
const grouped = groupBy(innerHitsSource, stat => `${stat.follower_index}:${stat.shard_id}`);
return {
...accum,
...grouped
};
}, {});
const buckets = get(response, 'aggregations.by_follower_index.buckets');
const data = buckets.reduce((accum, bucket) => {
const leaderIndex = get(bucket, 'leader_index.buckets[0].key');
let follows = leaderIndex;
if (follows.includes(':')) {
const followsSplit = follows.split(':');
follows = `${followsSplit[1]} on ${followsSplit[0]}`;
}
const stat = {
id: bucket.key,
index: bucket.key,
follows,
};
stat.shards = get(bucket, 'by_shard_id.buckets').reduce((accum, shardBucket) => {
const fullStat = get(fullStats[`${bucket.key}:${shardBucket.key}`], '[0]', {});
const shardStat = {
shardId: shardBucket.key,
error: fullStat.fetch_exceptions.length ? fullStat.fetch_exceptions[0].exception.type : null,
opsSynced: get(shardBucket, 'ops_synced.value'),
syncLagTime: get(shardBucket, 'last_fetch_time.value'),
syncLagOps: get(shardBucket, 'lag_ops.value'),
syncLagOpsLeader: get(shardBucket, 'leader_lag_ops.value'),
syncLagOpsFollower: get(shardBucket, 'follower_lag_ops.value'),
};
accum.push(shardStat);
return accum;
}, []);
stat.error = (stat.shards.find(shard => shard.error) || {}).error;
stat.opsSynced = stat.shards.reduce((sum, { opsSynced }) => sum + opsSynced, 0);
stat.syncLagTime = stat.shards.reduce((max, { syncLagTime }) => Math.max(max, syncLagTime), 0);
stat.syncLagOps = stat.shards.reduce((max, { syncLagOps }) => Math.max(max, syncLagOps), 0);
accum.push(stat);
return accum;
}, []);
reply({ data });
} catch(err) {
reply(handleError(err, req));
}
}
});
}

View file

@ -0,0 +1,149 @@
/*
* 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 moment from 'moment';
import Joi from 'joi';
import { handleError } from '../../../../lib/errors/handle_error';
import { prefixIndexPattern } from '../../../../lib/ccs_utils';
import { getMetrics } from '../../../../lib/details/get_metrics';
function getFormattedLeaderIndex(leaderIndex) {
let leader = leaderIndex;
if (leader.includes(':')) {
const leaderSplit = leader.split(':');
leader = `${leaderSplit[1]} on ${leaderSplit[0]}`;
}
return leader;
}
async function getCcrStat(req, esIndexPattern, filters) {
const min = moment.utc(req.payload.timeRange.min).valueOf();
const max = moment.utc(req.payload.timeRange.max).valueOf();
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const params = {
index: esIndexPattern,
size: 1,
filterPath: [
'hits.hits._source.ccr_stats',
'hits.hits._source.timestamp',
'hits.hits.inner_hits.oldest.hits.hits._source.ccr_stats.number_of_operations_indexed',
'hits.hits.inner_hits.oldest.hits.hits._source.ccr_stats.number_of_failed_fetches',
],
body: {
sort: [{ timestamp: { order: 'desc' } }],
query: {
bool: {
must: [
...filters,
{
range: {
timestamp: {
format: 'epoch_millis',
gte: min,
lte: max,
}
}
}
]
}
},
collapse: {
field: 'ccr_stats.follower_index',
inner_hits: {
name: 'oldest',
size: 1,
sort: [{ timestamp: 'asc' }]
}
}
}
};
return await callWithRequest(req, 'search', params);
}
export function ccrShardRoute(server) {
server.route({
method: 'POST',
path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr/{index}/shard/{shardId}',
config: {
validate: {
params: Joi.object({
clusterUuid: Joi.string().required(),
index: Joi.string().required(),
shardId: Joi.string().required()
}),
payload: Joi.object({
ccs: Joi.string().optional(),
timeRange: Joi.object({
min: Joi.date().required(),
max: Joi.date().required()
}).required(),
})
}
},
async handler(req, reply) {
const config = server.config();
const index = req.params.index;
const shardId = req.params.shardId;
const ccs = req.payload.ccs;
const esIndexPattern = prefixIndexPattern(config, 'xpack.monitoring.elasticsearch.index_pattern', ccs);
const filters = [
{
term: {
type: {
value: 'ccr_stats'
}
}
},
{
term: {
'ccr_stats.follower_index': {
value: index,
}
}
},
{
term: {
'ccr_stats.shard_id': {
value: shardId,
}
}
}
];
try {
const [
metrics,
ccrResponse
] = await Promise.all([
getMetrics(req, esIndexPattern, [
{ keys: ['ccr_sync_lag_time'], name: 'ccr_sync_lag_time' },
{ keys: ['ccr_sync_lag_ops'], name: 'ccr_sync_lag_ops' },
], filters),
getCcrStat(req, esIndexPattern, filters)
]);
const stat = get(ccrResponse, 'hits.hits[0]._source.ccr_stats', {});
const oldestStat = get(ccrResponse, 'hits.hits[0].inner_hits.oldest.hits.hits[0]._source.ccr_stats', {});
reply({
metrics,
stat,
formattedLeader: getFormattedLeaderIndex(stat.leader_index),
timestamp: get(ccrResponse, 'hits.hits[0]._source.timestamp'),
oldestStat,
});
} catch(err) {
reply(handleError(err, req));
}
}
});
}

View file

@ -10,3 +10,5 @@ export { esNodeRoute } from './node_detail';
export { esNodesRoute } from './nodes';
export { esOverviewRoute } from './overview';
export { mlJobRoute } from './ml_jobs';
export { ccrRoute } from './ccr';
export { ccrShardRoute } from './ccr_shard';

View file

@ -26,7 +26,9 @@ export {
esNodeRoute,
esNodesRoute,
esOverviewRoute,
mlJobRoute
mlJobRoute,
ccrRoute,
ccrShardRoute
} from './elasticsearch';
export {
clusterSettingsCheckRoute,

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import ccrFixture from './fixtures/ccr';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('ccr', () => {
const archive = 'monitoring/ccr';
const timeRange = {
min: '2018-09-19T00:00:00.000Z',
max: '2018-09-19T23:59:59.000Z'
};
before('load archive', () => {
return esArchiver.load(archive);
});
after('unload archive', () => {
return esArchiver.unload(archive);
});
it('should return all followers and a grouping of stats by follower index', async () => {
const { body } = await supertest
.post('/api/monitoring/v1/clusters/YCxj-RAgSZCP6GuOQ8M1EQ/elasticsearch/ccr')
.set('kbn-xsrf', 'xxx')
.send({
timeRange,
})
.expect(200);
expect(body).to.eql(ccrFixture);
});
});
}

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import ccrShardFixture from './fixtures/ccr_shard';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('ccr shard', () => {
const archive = 'monitoring/ccr';
const timeRange = {
min: '2018-09-19T00:00:00.000Z',
max: '2018-09-19T23:59:59.000Z'
};
before('load archive', () => {
return esArchiver.load(archive);
});
after('unload archive', () => {
return esArchiver.unload(archive);
});
it('should return specific shard details', async () => {
const { body } = await supertest
.post('/api/monitoring/v1/clusters/YCxj-RAgSZCP6GuOQ8M1EQ/elasticsearch/ccr/follower/shard/0')
.set('kbn-xsrf', 'xxx')
.send({
timeRange,
})
.expect(200);
expect(body).to.eql(ccrShardFixture);
});
});
}

View file

@ -0,0 +1,83 @@
{
"data": [{
"id": "follower2",
"index": "follower2",
"follows": "leader2",
"shards": [{
"shardId": 0,
"error": null,
"opsSynced": 52,
"syncLagTime": 59881,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}, {
"shardId": 1,
"error": null,
"opsSynced": 47,
"syncLagTime": 59959,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}, {
"shardId": 2,
"error": null,
"opsSynced": 51,
"syncLagTime": 55229,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}, {
"shardId": 3,
"error": null,
"opsSynced": 50,
"syncLagTime": 50483,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}, {
"shardId": 4,
"error": null,
"opsSynced": 55,
"syncLagTime": 55554,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}],
"opsSynced": 255,
"syncLagTime": 59959,
"syncLagOps": 0
}, {
"id": "follower",
"index": "follower",
"follows": "leader",
"shards": [{
"shardId": 0,
"error": null,
"opsSynced": 85,
"syncLagTime": 45513,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}, {
"shardId": 1,
"error": null,
"opsSynced": 94,
"syncLagTime": 55205,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}, {
"shardId": 2,
"error": null,
"opsSynced": 76,
"syncLagTime": 50003,
"syncLagOps": 0,
"syncLagOpsLeader": 0,
"syncLagOpsFollower": 0
}],
"opsSynced": 255,
"syncLagTime": 55205,
"syncLagOps": 0
}]
}

View file

@ -0,0 +1,75 @@
{
"formattedLeader": "leader",
"metrics": {
"ccr_sync_lag_time": [{
"bucket_size": "10 min",
"timeRange": {
"min": 1537315200000,
"max": 1537401599000
},
"metric": {
"app": "elasticsearch",
"field": "ccr_stats.time_since_last_fetch_millis",
"metricAgg": "max",
"label": "Fetch delay",
"title": "Fetch delay",
"description": "The amount of time the follower index is lagging behind the leader.",
"units": "s",
"format": "0.[00]",
"hasCalculation": true,
"isDerivative": false
},
"data": []
}],
"ccr_sync_lag_ops": [{
"bucket_size": "10 min",
"timeRange": {
"min": 1537315200000,
"max": 1537401599000
},
"metric": {
"app": "elasticsearch",
"field": "",
"metricAgg": "sum",
"label": "Ops delay",
"title": "Ops delay",
"description": "The amount of time the follower index is lagging behind the leader.",
"units": "ms",
"format": "0,0.[00]",
"hasCalculation": true,
"isDerivative": false
},
"data": []
}]
},
"stat": {
"leader_index": "leader",
"follower_index": "follower",
"shard_id": 0,
"leader_global_checkpoint": 85,
"leader_max_seq_no": 85,
"follower_global_checkpoint": 85,
"follower_max_seq_no": 85,
"last_requested_seq_no": 85,
"number_of_concurrent_reads": 1,
"number_of_concurrent_writes": 0,
"number_of_queued_writes": 0,
"mapping_version": 2,
"total_fetch_time_millis": 1265908,
"number_of_successful_fetches": 86,
"number_of_failed_fetches": 0,
"operations_received": 86,
"total_transferred_bytes": 6602,
"total_index_time_millis": 1096,
"number_of_successful_bulk_operations": 86,
"number_of_failed_bulk_operations": 0,
"number_of_operations_indexed": 86,
"fetch_exceptions": [],
"time_since_last_fetch_millis": 19886
},
"timestamp": "2018-09-19T20:01:00.440Z",
"oldestStat": {
"number_of_failed_fetches": 0,
"number_of_operations_indexed": 1
}
}

View file

@ -12,5 +12,7 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./node_detail_advanced'));
loadTestFile(require.resolve('./indices'));
loadTestFile(require.resolve('./index_detail'));
loadTestFile(require.resolve('./ccr'));
loadTestFile(require.resolve('./ccr_shard'));
});
}

File diff suppressed because it is too large Load diff