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); }