diff --git a/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.js
index 0b2eb94de9b12..8d8848ed08acb 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.js
@@ -7,5 +7,6 @@
export const SECTIONS = {
AUTO_FOLLOW_PATTERN: 'autoFollowPattern',
INDEX_FOLLOWER: 'indexFollower',
- REMOTE_CLUSTER: 'remoteCluster'
+ REMOTE_CLUSTER: 'remoteCluster',
+ CCR_STATS: 'ccrStats',
};
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
index efed7e2593c97..eb2ba69c3ed99 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
@@ -18,6 +18,7 @@ import {
loadAutoFollowPatterns,
openAutoFollowPatternDetailPanel as openDetailPanel,
closeAutoFollowPatternDetailPanel as closeDetailPanel,
+ loadAutoFollowStats,
} from '../../../store/actions';
import { AutoFollowPatternList as AutoFollowPatternListView } from './auto_follow_pattern_list';
@@ -39,6 +40,7 @@ const mapDispatchToProps = dispatch => ({
closeDetailPanel: () => {
dispatch(closeDetailPanel());
},
+ loadAutoFollowStats: () => dispatch(loadAutoFollowStats())
});
export const AutoFollowPatternList = connect(
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
index 34768f655be69..f20ae97ac7d7f 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
@@ -21,6 +21,7 @@ export const AutoFollowPatternList = injectI18n(
class extends PureComponent {
static propTypes = {
loadAutoFollowPatterns: PropTypes.func,
+ loadAutoFollowStats: PropTypes.func,
autoFollowPatterns: PropTypes.array,
apiStatus: PropTypes.string,
apiError: PropTypes.object,
@@ -31,6 +32,7 @@ export const AutoFollowPatternList = injectI18n(
componentDidMount() {
this.props.loadAutoFollowPatterns();
+ this.props.loadAutoFollowStats();
// Interval to load auto-follow patterns in the background passing "true" to the fetch method
this.interval = setInterval(() => this.props.loadAutoFollowPatterns(true), REFRESH_RATE_MS);
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
index c2c239960743d..2af00a8babd32 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
@@ -181,11 +181,54 @@ export class DetailPanelUi extends Component {
/>
+
+ {this.renderAutoFollowPatternErrors()}
);
}
+ renderAutoFollowPatternErrors() {
+ const { autoFollowPattern } = this.props;
+
+ if (!autoFollowPattern.errors.length) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {autoFollowPattern.errors.map((error, i) => (
+ - {error.autoFollowException.reason}
+ ))}
+
+
+
+ );
+ }
+
renderContent() {
const {
apiStatus,
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/api.js b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js
index f6d4a8f084b73..1281d369d679d 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/services/api.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js
@@ -48,3 +48,7 @@ export const deleteAutoFollowPattern = (id) => {
return httpClient.delete(`${apiPrefix}/auto_follow_patterns/${ids}`).then(extractData);
};
+
+export const loadAutoFollowStats = () => (
+ httpClient.get(`${apiPrefix}/stats/auto-follow`).then(extractData)
+);
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js
new file mode 100644
index 0000000000000..b41eb685d1fbf
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+export const parseAutoFollowError = (error) => {
+ if (!error.leaderIndex) {
+ return null;
+ }
+
+ const { leaderIndex, autoFollowException } = error;
+ const id = leaderIndex.substring(0, leaderIndex.lastIndexOf(':'));
+
+ return {
+ id,
+ leaderIndex,
+ autoFollowException
+ };
+};
+
+/**
+ * Parse an array of auto-follow pattern errors and returns
+ * an object where each key is an auto-follow pattern id
+ */
+export const parseAutoFollowErrors = (recentAutoFollowErrors, maxErrorsToShow = 5) => (
+ recentAutoFollowErrors
+ .map(parseAutoFollowError)
+ .filter(error => error !== null)
+ .reduce((byId, error) => {
+ if (!byId[error.id]) {
+ byId[error.id] = [];
+ }
+
+ if (byId[error.id].length === maxErrorsToShow) {
+ return byId;
+ }
+
+ byId[error.id].push(error);
+ return byId;
+ }, {})
+);
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js
new file mode 100644
index 0000000000000..853413f0d3da8
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js
@@ -0,0 +1,57 @@
+/*
+ * 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 { parseAutoFollowErrors } from './auto_follow_errors';
+
+describe('Auto-follow pattern errors service', () => {
+ it('should convert an array of error to an object where each key is an auto-follow pattern id', () => {
+ const esErrors = [{
+ leaderIndex: 'some:id::kibana_sample_4',
+ autoFollowException: { type: 'exception', reason: 'Error 1' }
+ },
+ {
+ leaderIndex: 'another-id:mock:kibana_sample_5',
+ autoFollowException: { type: 'exception', reason: 'Error 2' }
+ }, {
+ leaderIndex: 'some:id::kibana_sample_5',
+ autoFollowException: { type: 'exception', reason: 'Error 3' }
+ }];
+
+ const expected = {
+ 'another-id:mock': [{
+ id: 'another-id:mock',
+ leaderIndex: 'another-id:mock:kibana_sample_5',
+ autoFollowException: { type: 'exception', reason: 'Error 2' }
+ }],
+ 'some:id:': [{
+ id: 'some:id:',
+ leaderIndex: 'some:id::kibana_sample_4',
+ autoFollowException: { type: 'exception', reason: 'Error 1' }
+ }, {
+ id: 'some:id:',
+ leaderIndex: 'some:id::kibana_sample_5',
+ autoFollowException: { type: 'exception', reason: 'Error 3' }
+ }],
+ };
+
+ expect(parseAutoFollowErrors(esErrors)).toEqual(expected);
+ });
+
+ it('should limit the number of error to show for each pattern', () => {
+ const esErrors = [
+ { leaderIndex: 'my-id:kibana-1' },
+ { leaderIndex: 'my-id:kibana-2' },
+ { leaderIndex: 'my-id:kibana-3' },
+ { leaderIndex: 'my-id:kibana-4' },
+ { leaderIndex: 'my-id:kibana-5' },
+ { leaderIndex: 'my-id:kibana-6' },
+ { leaderIndex: 'my-id:kibana-7' },
+ ];
+
+ expect(parseAutoFollowErrors(esErrors)['my-id'].length).toEqual(5);
+ });
+
+});
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js b/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js
index dd618161aefce..875a582580bb2 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js
@@ -18,3 +18,6 @@ export const AUTO_FOLLOW_PATTERN_CREATE = 'AUTO_FOLLOW_PATTERN_CREATE';
export const AUTO_FOLLOW_PATTERN_UPDATE = 'AUTO_FOLLOW_PATTERN_UPDATE';
export const AUTO_FOLLOW_PATTERN_DELETE = 'AUTO_FOLLOW_PATTERN_DELETE';
export const AUTO_FOLLOW_PATTERN_DETAIL_PANEL = 'AUTO_FOLLOW_PATTERN_DETAIL_PANEL';
+
+// Stats
+export const AUTO_FOLLOW_STATS_LOAD = 'AUTO_FOLLOW_STATS_LOAD';
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js
new file mode 100644
index 0000000000000..165fcb56be8c9
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js
@@ -0,0 +1,20 @@
+/*
+ * 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 { SECTIONS } from '../../constants';
+import { loadAutoFollowStats as loadAutoFollowStatsRequest } from '../../services/api';
+import * as t from '../action_types';
+import { sendApiRequest } from './api';
+
+const { CCR_STATS: scope } = SECTIONS;
+
+export const loadAutoFollowStats = () =>
+ sendApiRequest({
+ label: t.AUTO_FOLLOW_STATS_LOAD,
+ scope,
+ handler: async () => (
+ await loadAutoFollowStatsRequest()
+ ),
+ });
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js
index d45c328f5d830..75f2344f76e06 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js
@@ -7,3 +7,4 @@
export * from './auto_follow_pattern';
export * from './api';
+export * from './ccr';
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/middleware/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/middleware/auto_follow_pattern.js
index 214f4768be7a7..dbb7985b409eb 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/store/middleware/auto_follow_pattern.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/middleware/auto_follow_pattern.js
@@ -7,8 +7,9 @@
import routing from '../../services/routing';
import * as t from '../action_types';
import { extractQueryParams } from '../../services/query_params';
+import { loadAutoFollowStats } from '../actions';
-export const autoFollowPatternMiddleware = () => next => action => {
+export const autoFollowPatternMiddleware = ({ dispatch }) => next => action => {
const { type, payload: name } = action;
const { history } = routing.reactRouter;
const search = history.location.search;
@@ -28,9 +29,10 @@ export const autoFollowPatternMiddleware = () => next => action => {
history.replace({
search: `?pattern=${encodeURIComponent(name)}`,
});
+
+ dispatch(loadAutoFollowStats());
}
}
-
break;
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js
index b34d824c3f278..2dc9d42fc2b8d 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js
@@ -7,8 +7,10 @@
import { combineReducers } from 'redux';
import { reducer as api } from './api';
import { reducer as autoFollowPattern } from './auto_follow_pattern';
+import { reducer as stats } from './stats';
export const ccr = combineReducers({
autoFollowPattern,
api,
+ stats,
});
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js
new file mode 100644
index 0000000000000..67dd7df99ce60
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js
@@ -0,0 +1,28 @@
+/*
+ * 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 * as t from '../action_types';
+import { parseAutoFollowErrors } from '../../services/auto_follow_errors';
+
+const initialState = {
+ autoFollow: null,
+};
+
+const success = action => `${action}_SUCCESS`;
+
+export const reducer = (state = initialState, action) => {
+ switch (action.type) {
+ case success(t.AUTO_FOLLOW_STATS_LOAD): {
+ const { recentAutoFollowErrors, ...rest } = action.payload;
+ return { ...state, autoFollow: {
+ ...rest,
+ recentAutoFollowErrors: parseAutoFollowErrors(recentAutoFollowErrors)
+ } };
+ }
+ default:
+ return state;
+ }
+};
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js
index bf680fb157879..0bb0b099b8701 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js
@@ -18,6 +18,10 @@ export const isApiAuthorized = (scope) => createSelector(getApiError(scope), (er
return error.status !== 403;
});
+// Stats
+export const getStatsState = (state) => state.stats;
+export const getAutoFollowStats = createSelector(getStatsState, (statsState) => statsState.autoFollow);
+
// Auto-follow pattern
export const getAutoFollowPatternState = (state) => state.autoFollowPattern;
export const getAutoFollowPatterns = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => autoFollowPatternsState.byId);
@@ -32,10 +36,15 @@ export const getSelectedAutoFollowPattern = createSelector(getAutoFollowPatternS
export const isAutoFollowPatternDetailPanelOpen = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => {
return !!autoFollowPatternsState.detailPanelId;
});
-export const getDetailPanelAutoFollowPattern = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => {
- if(!autoFollowPatternsState.detailPanelId) {
- return null;
- }
- return autoFollowPatternsState.byId[autoFollowPatternsState.detailPanelId];
-});
+export const getDetailPanelAutoFollowPattern = createSelector(
+ getAutoFollowPatternState, getAutoFollowStats, (autoFollowPatternsState, autoFollowStatsState) => {
+ if(!autoFollowPatternsState.detailPanelId) {
+ return null;
+ }
+ const { detailPanelId } = autoFollowPatternsState;
+ const autoFollowPattern = autoFollowPatternsState.byId[detailPanelId];
+ const errors = autoFollowStatsState && autoFollowStatsState.recentAutoFollowErrors[detailPanelId] || [];
+ return autoFollowPattern ? { ...autoFollowPattern, errors } : null;
+ });
export const getListAutoFollowPatterns = createSelector(getAutoFollowPatterns, (autoFollowPatterns) => objectToArray(autoFollowPatterns));
+
diff --git a/x-pack/plugins/cross_cluster_replication/public/register_routes.js b/x-pack/plugins/cross_cluster_replication/public/register_routes.js
index 46f75b23905a2..d8084f1d3415c 100644
--- a/x-pack/plugins/cross_cluster_replication/public/register_routes.js
+++ b/x-pack/plugins/cross_cluster_replication/public/register_routes.js
@@ -26,7 +26,7 @@ if (chrome.getInjected('ccrUiEnabled')) {
controller: class CrossClusterReplicationController {
constructor($scope, $route, $http) {
/**
- * React-router's does not play wall with the angular router. It will cause this controller
+ * React-router's does not play well with the angular router. It will cause this controller
* to re-execute without the $destroy handler being called. This means that the app will be mounted twice
* creating a memory leak when leaving (only 1 app will be unmounted).
* To avoid this, we unmount the React app each time we enter the controller.
diff --git a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js
index 146d0ba559748..c3ed6570c44c9 100644
--- a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js
+++ b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js
@@ -62,4 +62,13 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
needBody: true,
method: 'DELETE'
});
+
+ ccr.stats = ca({
+ urls: [
+ {
+ fmt: '/_ccr/stats',
+ }
+ ],
+ method: 'GET'
+ });
};
diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.js.snap b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.js.snap
new file mode 100644
index 0000000000000..92ac6070904b5
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.js.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`[CCR] auto-follow stats serialization should deserialize auto-follow stats 1`] = `
+Object {
+ "autoFollowedClusters": Array [
+ Object {
+ "clusterName": "new-york",
+ "lastSeenMetadataVersion": 15,
+ "timeSinceLastCheckMillis": 2426,
+ },
+ ],
+ "numberOfFailedFollowIndices": 0,
+ "numberOfFailedRemoteClusterStateRequests": 0,
+ "numberOfSuccessfulFollowIndices": 0,
+ "recentAutoFollowErrors": Array [
+ Object {
+ "autoFollowException": Object {
+ "reason": "index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]",
+ "type": "exception",
+ },
+ "leaderIndex": "pattern-1:kibana_sample_1",
+ },
+ Object {
+ "autoFollowException": Object {
+ "reason": "index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]",
+ "type": "exception",
+ },
+ "leaderIndex": "pattern-2:kibana_sample_1",
+ },
+ ],
+}
+`;
diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap
new file mode 100644
index 0000000000000..1c20f73287259
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/follower_index_serialization.test.js.snap
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = `
+Object {
+ "name": "follower index name",
+ "shards": Array [
+ Object {
+ "bytesReadCount": undefined,
+ "failedReadRequestsCount": undefined,
+ "failedWriteRequestsCount": undefined,
+ "followerGlobalCheckpoint": undefined,
+ "followerMappingVersion": undefined,
+ "followerMaxSequenceNum": undefined,
+ "followerSettingsVersion": undefined,
+ "id": "shard 1",
+ "lastRequestedSequenceNum": undefined,
+ "leaderGlobalCheckpoint": undefined,
+ "leaderIndex": undefined,
+ "leaderMaxSequenceNum": undefined,
+ "operationsReadCount": undefined,
+ "operationsWrittenCount": undefined,
+ "outstandingReadRequestsCount": undefined,
+ "outstandingWriteRequestsCount": undefined,
+ "readExceptions": undefined,
+ "remoteCluster": undefined,
+ "successfulReadRequestCount": undefined,
+ "successfulWriteRequestsCount": undefined,
+ "timeSinceLastReadMs": undefined,
+ "totalReadRemoteExecTimeMs": undefined,
+ "totalReadTimeMs": undefined,
+ "totalWriteTimeMs": undefined,
+ "writeBufferOperationsCount": undefined,
+ "writeBufferSizeBytes": undefined,
+ },
+ Object {
+ "bytesReadCount": undefined,
+ "failedReadRequestsCount": undefined,
+ "failedWriteRequestsCount": undefined,
+ "followerGlobalCheckpoint": undefined,
+ "followerMappingVersion": undefined,
+ "followerMaxSequenceNum": undefined,
+ "followerSettingsVersion": undefined,
+ "id": "shard 2",
+ "lastRequestedSequenceNum": undefined,
+ "leaderGlobalCheckpoint": undefined,
+ "leaderIndex": undefined,
+ "leaderMaxSequenceNum": undefined,
+ "operationsReadCount": undefined,
+ "operationsWrittenCount": undefined,
+ "outstandingReadRequestsCount": undefined,
+ "outstandingWriteRequestsCount": undefined,
+ "readExceptions": undefined,
+ "remoteCluster": undefined,
+ "successfulReadRequestCount": undefined,
+ "successfulWriteRequestsCount": undefined,
+ "timeSinceLastReadMs": undefined,
+ "totalReadRemoteExecTimeMs": undefined,
+ "totalReadTimeMs": undefined,
+ "totalWriteTimeMs": undefined,
+ "writeBufferOperationsCount": undefined,
+ "writeBufferSizeBytes": undefined,
+ },
+ ],
+}
+`;
+
+exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = `
+Object {
+ "bytesReadCount": "bytes read",
+ "failedReadRequestsCount": "failed read requests",
+ "failedWriteRequestsCount": "failed write requests",
+ "followerGlobalCheckpoint": "follower global checkpoint",
+ "followerMappingVersion": "follower mapping version",
+ "followerMaxSequenceNum": "follower max seq no",
+ "followerSettingsVersion": "follower settings version",
+ "id": "shard id",
+ "lastRequestedSequenceNum": "last requested seq no",
+ "leaderGlobalCheckpoint": "leader global checkpoint",
+ "leaderIndex": "leader index",
+ "leaderMaxSequenceNum": "leader max seq no",
+ "operationsReadCount": "operations read",
+ "operationsWrittenCount": "operations written",
+ "outstandingReadRequestsCount": "outstanding read requests",
+ "outstandingWriteRequestsCount": "outstanding write requests",
+ "readExceptions": Array [
+ "read exception",
+ ],
+ "remoteCluster": "remote cluster",
+ "successfulReadRequestCount": "successful read requests",
+ "successfulWriteRequestsCount": "successful write requests",
+ "timeSinceLastReadMs": "time since last read millis",
+ "totalReadRemoteExecTimeMs": "total read remote exec time millis",
+ "totalReadTimeMs": "total read time millis",
+ "totalWriteTimeMs": "total write time millis",
+ "writeBufferOperationsCount": "write buffer operation count",
+ "writeBufferSizeBytes": "write buffer size in bytes",
+}
+`;
diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.js b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.js
new file mode 100644
index 0000000000000..682bf73e22291
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/* eslint-disable camelcase */
+export const deserializeRecentAutoFollowErrors = ({
+ leader_index,
+ auto_follow_exception: {
+ type,
+ reason
+ }
+}) => ({
+ leaderIndex: leader_index,
+ autoFollowException: {
+ type,
+ reason
+ }
+});
+
+export const deserializeAutoFollowedClusters = ({
+ cluster_name,
+ time_since_last_check_millis,
+ last_seen_metadata_version
+}) => ({
+ clusterName: cluster_name,
+ timeSinceLastCheckMillis: time_since_last_check_millis,
+ lastSeenMetadataVersion: last_seen_metadata_version,
+});
+
+export const deserializeAutoFollowStats = ({
+ number_of_failed_follow_indices,
+ number_of_failed_remote_cluster_state_requests,
+ number_of_successful_follow_indices,
+ recent_auto_follow_errors,
+ auto_followed_clusters
+}) => ({
+ numberOfFailedFollowIndices: number_of_failed_follow_indices,
+ numberOfFailedRemoteClusterStateRequests: number_of_failed_remote_cluster_state_requests,
+ numberOfSuccessfulFollowIndices: number_of_successful_follow_indices,
+ recentAutoFollowErrors: recent_auto_follow_errors.map(deserializeRecentAutoFollowErrors),
+ autoFollowedClusters: auto_followed_clusters.map(deserializeAutoFollowedClusters),
+});
+/* eslint-enable camelcase */
diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.js b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.js
new file mode 100644
index 0000000000000..d315f5df55a88
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.js
@@ -0,0 +1,40 @@
+/*
+ * 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 { deserializeAutoFollowStats } from './ccr_stats_serialization';
+
+describe('[CCR] auto-follow stats serialization', () => {
+ it('should deserialize auto-follow stats', () => {
+ const esObject = {
+ "number_of_failed_follow_indices": 0,
+ "number_of_failed_remote_cluster_state_requests": 0,
+ "number_of_successful_follow_indices": 0,
+ "recent_auto_follow_errors": [
+ {
+ "leader_index": "pattern-1:kibana_sample_1",
+ "auto_follow_exception": {
+ "type": "exception",
+ "reason": "index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]"
+ }
+ },
+ {
+ "leader_index": "pattern-2:kibana_sample_1",
+ "auto_follow_exception": {
+ "type": "exception",
+ "reason": "index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]"
+ }
+ }
+ ],
+ "auto_followed_clusters": [{
+ "cluster_name": "new-york",
+ "time_since_last_check_millis": 2426,
+ "last_seen_metadata_version": 15
+ }]
+ };
+
+ expect(deserializeAutoFollowStats(esObject)).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js
new file mode 100644
index 0000000000000..7b5a0e453b65f
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.js
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable camelcase */
+export const deserializeShard = ({
+ remote_cluster,
+ leader_index,
+ shard_id,
+ leader_global_checkpoint,
+ leader_max_seq_no,
+ follower_global_checkpoint,
+ follower_max_seq_no,
+ last_requested_seq_no,
+ outstanding_read_requests,
+ outstanding_write_requests,
+ write_buffer_operation_count,
+ write_buffer_size_in_bytes,
+ follower_mapping_version,
+ follower_settings_version,
+ total_read_time_millis,
+ total_read_remote_exec_time_millis,
+ successful_read_requests,
+ failed_read_requests,
+ operations_read,
+ bytes_read,
+ total_write_time_millis,
+ successful_write_requests,
+ failed_write_requests,
+ operations_written,
+ read_exceptions,
+ time_since_last_read_millis,
+}) => ({
+ id: shard_id,
+ remoteCluster: remote_cluster,
+ leaderIndex: leader_index,
+ leaderGlobalCheckpoint: leader_global_checkpoint,
+ leaderMaxSequenceNum: leader_max_seq_no,
+ followerGlobalCheckpoint: follower_global_checkpoint,
+ followerMaxSequenceNum: follower_max_seq_no,
+ lastRequestedSequenceNum: last_requested_seq_no,
+ outstandingReadRequestsCount: outstanding_read_requests,
+ outstandingWriteRequestsCount: outstanding_write_requests,
+ writeBufferOperationsCount: write_buffer_operation_count,
+ writeBufferSizeBytes: write_buffer_size_in_bytes,
+ followerMappingVersion: follower_mapping_version,
+ followerSettingsVersion: follower_settings_version,
+ totalReadTimeMs: total_read_time_millis,
+ totalReadRemoteExecTimeMs: total_read_remote_exec_time_millis,
+ successfulReadRequestCount: successful_read_requests,
+ failedReadRequestsCount: failed_read_requests,
+ operationsReadCount: operations_read,
+ bytesReadCount: bytes_read,
+ totalWriteTimeMs: total_write_time_millis,
+ successfulWriteRequestsCount: successful_write_requests,
+ failedWriteRequestsCount: failed_write_requests,
+ operationsWrittenCount: operations_written,
+ // This is an array of exception objects
+ readExceptions: read_exceptions,
+ timeSinceLastReadMs: time_since_last_read_millis,
+});
+/* eslint-enable camelcase */
+
+export const deserializeFollowerIndex = ({ index, shards }) => ({
+ name: index,
+ shards: shards.map(deserializeShard),
+});
+
+export const deserializeListFollowerIndices = followerIndices =>
+ followerIndices.map(deserializeFollowerIndex);
+
+export const serializeFollowerIndex = ({ remoteCluster, leaderIndex }) => ({
+ remote_cluster: remoteCluster,
+ leader_index: leaderIndex,
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js
new file mode 100644
index 0000000000000..d4b5526c16a6b
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/lib/follower_index_serialization.test.js
@@ -0,0 +1,103 @@
+/*
+ * 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 {
+ deserializeShard,
+ deserializeFollowerIndex,
+ deserializeListFollowerIndices,
+ serializeFollowerIndex,
+} from './follower_index_serialization';
+
+describe('[CCR] follower index serialization', () => {
+ describe('deserializeShard()', () => {
+ it('deserializes shard', () => {
+ const serializedShard = {
+ remote_cluster: 'remote cluster',
+ leader_index: 'leader index',
+ shard_id: 'shard id',
+ leader_global_checkpoint: 'leader global checkpoint',
+ leader_max_seq_no: 'leader max seq no',
+ follower_global_checkpoint: 'follower global checkpoint',
+ follower_max_seq_no: 'follower max seq no',
+ last_requested_seq_no: 'last requested seq no',
+ outstanding_read_requests: 'outstanding read requests',
+ outstanding_write_requests: 'outstanding write requests',
+ write_buffer_operation_count: 'write buffer operation count',
+ write_buffer_size_in_bytes: 'write buffer size in bytes',
+ follower_mapping_version: 'follower mapping version',
+ follower_settings_version: 'follower settings version',
+ total_read_time_millis: 'total read time millis',
+ total_read_remote_exec_time_millis: 'total read remote exec time millis',
+ successful_read_requests: 'successful read requests',
+ failed_read_requests: 'failed read requests',
+ operations_read: 'operations read',
+ bytes_read: 'bytes read',
+ total_write_time_millis: 'total write time millis',
+ successful_write_requests: 'successful write requests',
+ failed_write_requests: 'failed write requests',
+ operations_written: 'operations written',
+ read_exceptions: ['read exception'],
+ time_since_last_read_millis: 'time since last read millis',
+ };
+
+ expect(deserializeShard(serializedShard)).toMatchSnapshot();
+ });
+ });
+
+ describe('deserializeFollowerIndex()', () => {
+ it('deserializes Elasticsearch follower index object', () => {
+ const serializedFollowerIndex = {
+ index: 'follower index name',
+ shards: [{
+ shard_id: 'shard 1',
+ }, {
+ shard_id: 'shard 2',
+ }],
+ };
+
+ expect(deserializeFollowerIndex(serializedFollowerIndex)).toMatchSnapshot();
+ });
+ });
+
+ describe('deserializeListFollowerIndices()', () => {
+ it('deserializes list of Elasticsearch follower index objects', () => {
+ const serializedFollowerIndexList = [{
+ index: 'follower index 1',
+ shards: [],
+ }, {
+ index: 'follower index 2',
+ shards: [],
+ }];
+
+ const deserializedFollowerIndexList = [{
+ name: 'follower index 1',
+ shards: [],
+ }, {
+ name: 'follower index 2',
+ shards: [],
+ }];
+
+ expect(deserializeListFollowerIndices(serializedFollowerIndexList))
+ .toEqual(deserializedFollowerIndexList);
+ });
+ });
+
+ describe('serializeFollowerIndex()', () => {
+ it('serializes object to Elasticsearch follower index object', () => {
+ const deserializedFollowerIndex = {
+ remoteCluster: 'remote cluster',
+ leaderIndex: 'leader index',
+ };
+
+ const serializedFollowerIndex = {
+ remote_cluster: 'remote cluster',
+ leader_index: 'leader index',
+ };
+
+ expect(serializeFollowerIndex(deserializedFollowerIndex)).toEqual(serializedFollowerIndex);
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js b/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js
new file mode 100644
index 0000000000000..781f4d6ec6cd5
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/ccr.js
@@ -0,0 +1,63 @@
+/*
+ * 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 { callWithRequestFactory } from '../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers';
+import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization';
+import { deserializeListFollowerIndices } from '../../lib/follower_index_serialization';
+import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory';
+import { API_BASE_PATH } from '../../../common/constants';
+
+export const registerCcrRoutes = (server) => {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ const getStatsHandler = async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await callWithRequest('ccr.stats');
+ return {
+ autoFollow: deserializeAutoFollowStats(response.auto_follow_stats),
+ follow: {
+ indices: deserializeListFollowerIndices(response.follow_stats.indices)
+ }
+ };
+ } catch(err) {
+ if (isEsError(err)) {
+ throw wrapEsError(err);
+ }
+ throw wrapUnknownError(err);
+ }
+ };
+
+ /**
+ * Returns CCR stats
+ */
+ server.route({
+ path: `${API_BASE_PATH}/stats`,
+ method: 'GET',
+ config: {
+ pre: [ licensePreRouting ]
+ },
+ handler: getStatsHandler,
+ });
+
+ /**
+ * Returns Auto-follow stats
+ */
+ server.route({
+ path: `${API_BASE_PATH}/stats/auto-follow`,
+ method: 'GET',
+ config: {
+ pre: [ licensePreRouting ]
+ },
+ handler: async (request) => {
+ const { autoFollow } = await getStatsHandler(request);
+ return autoFollow;
+ },
+ });
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/register_routes.js b/x-pack/plugins/cross_cluster_replication/server/routes/register_routes.js
index 486f9784c3367..fec6d152f160c 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/register_routes.js
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/register_routes.js
@@ -5,7 +5,9 @@
*/
import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern';
+import { registerCcrRoutes } from './api/ccr';
export function registerRoutes(server) {
registerAutoFollowPatternRoutes(server);
+ registerCcrRoutes(server);
}