Przeglądaj źródła

[HLRC] Added support for CCR Stats API (#36213)

This change also adds documentation for the CCR Stats API.

Relates to #33824
Martijn van Groningen 6 lat temu
rodzic
commit
786697a4b2

+ 46 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java

@@ -20,6 +20,8 @@
 package org.elasticsearch.client;
 
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.client.ccr.CcrStatsRequest;
+import org.elasticsearch.client.ccr.CcrStatsResponse;
 import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse;
@@ -360,4 +362,48 @@ public final class CcrClient {
         );
     }
 
+    /**
+     * Gets all CCR stats.
+     *
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-stats.html">
+     * the docs</a> for more.
+     *
+     * @param request the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public CcrStatsResponse getCcrStats(CcrStatsRequest request,
+                                        RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(
+            request,
+            CcrRequestConverters::getCcrStats,
+            options,
+            CcrStatsResponse::fromXContent,
+            Collections.emptySet()
+        );
+    }
+
+    /**
+     * Gets all CCR stats.
+     *
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-stats.html">
+     * the docs</a> for more.
+     *
+     * @param request the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     */
+    public void getCcrStatsAsync(CcrStatsRequest request,
+                                 RequestOptions options,
+                                 ActionListener<CcrStatsResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(
+            request,
+            CcrRequestConverters::getCcrStats,
+            options,
+            CcrStatsResponse::fromXContent,
+            listener,
+            Collections.emptySet()
+        );
+    }
+
 }

+ 8 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java

@@ -23,6 +23,7 @@ import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
+import org.elasticsearch.client.ccr.CcrStatsRequest;
 import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.PauseFollowRequest;
@@ -100,4 +101,11 @@ final class CcrRequestConverters {
         return new Request(HttpGet.METHOD_NAME, endpoint);
     }
 
+    static Request getCcrStats(CcrStatsRequest ccrStatsRequest) {
+        String endpoint = new RequestConverters.EndpointBuilder()
+            .addPathPartAsIs("_ccr", "stats")
+            .build();
+        return new Request(HttpGet.METHOD_NAME, endpoint);
+    }
+
 }

+ 105 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/AutoFollowStats.java

@@ -0,0 +1,105 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.ccr;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+public final class AutoFollowStats {
+
+    static final ParseField NUMBER_OF_SUCCESSFUL_INDICES_AUTO_FOLLOWED = new ParseField("number_of_successful_follow_indices");
+    static final ParseField NUMBER_OF_FAILED_INDICES_AUTO_FOLLOWED = new ParseField("number_of_failed_follow_indices");
+    static final ParseField NUMBER_OF_FAILED_REMOTE_CLUSTER_STATE_REQUESTS =
+        new ParseField("number_of_failed_remote_cluster_state_requests");
+    static final ParseField RECENT_AUTO_FOLLOW_ERRORS = new ParseField("recent_auto_follow_errors");
+    static final ParseField LEADER_INDEX = new ParseField("leader_index");
+    static final ParseField AUTO_FOLLOW_EXCEPTION = new ParseField("auto_follow_exception");
+
+    @SuppressWarnings("unchecked")
+    static final ConstructingObjectParser<AutoFollowStats, Void> STATS_PARSER = new ConstructingObjectParser<>("auto_follow_stats",
+        args -> new AutoFollowStats(
+            (Long) args[0],
+            (Long) args[1],
+            (Long) args[2],
+            new TreeMap<>(
+                ((List<Map.Entry<String, ElasticsearchException>>) args[3])
+                    .stream()
+                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
+        ));
+
+    private static final ConstructingObjectParser<Map.Entry<String, ElasticsearchException>, Void> AUTO_FOLLOW_EXCEPTIONS_PARSER =
+        new ConstructingObjectParser<>(
+            "auto_follow_stats_errors",
+            args -> new AbstractMap.SimpleEntry<>((String) args[0], (ElasticsearchException) args[1]));
+
+    static {
+        AUTO_FOLLOW_EXCEPTIONS_PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_INDEX);
+        AUTO_FOLLOW_EXCEPTIONS_PARSER.declareObject(
+            ConstructingObjectParser.constructorArg(),
+            (p, c) -> ElasticsearchException.fromXContent(p),
+            AUTO_FOLLOW_EXCEPTION);
+
+        STATS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_INDICES_AUTO_FOLLOWED);
+        STATS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_REMOTE_CLUSTER_STATE_REQUESTS);
+        STATS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_SUCCESSFUL_INDICES_AUTO_FOLLOWED);
+        STATS_PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), AUTO_FOLLOW_EXCEPTIONS_PARSER,
+            RECENT_AUTO_FOLLOW_ERRORS);
+    }
+
+    private final long numberOfFailedFollowIndices;
+    private final long numberOfFailedRemoteClusterStateRequests;
+    private final long numberOfSuccessfulFollowIndices;
+    private final NavigableMap<String, ElasticsearchException> recentAutoFollowErrors;
+
+    AutoFollowStats(long numberOfFailedFollowIndices,
+                    long numberOfFailedRemoteClusterStateRequests,
+                    long numberOfSuccessfulFollowIndices,
+                    NavigableMap<String, ElasticsearchException> recentAutoFollowErrors) {
+        this.numberOfFailedFollowIndices = numberOfFailedFollowIndices;
+        this.numberOfFailedRemoteClusterStateRequests = numberOfFailedRemoteClusterStateRequests;
+        this.numberOfSuccessfulFollowIndices = numberOfSuccessfulFollowIndices;
+        this.recentAutoFollowErrors = recentAutoFollowErrors;
+    }
+
+    public long getNumberOfFailedFollowIndices() {
+        return numberOfFailedFollowIndices;
+    }
+
+    public long getNumberOfFailedRemoteClusterStateRequests() {
+        return numberOfFailedRemoteClusterStateRequests;
+    }
+
+    public long getNumberOfSuccessfulFollowIndices() {
+        return numberOfSuccessfulFollowIndices;
+    }
+
+    public NavigableMap<String, ElasticsearchException> getRecentAutoFollowErrors() {
+        return recentAutoFollowErrors;
+    }
+
+}

+ 25 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/CcrStatsRequest.java

@@ -0,0 +1,25 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.ccr;
+
+import org.elasticsearch.client.Validatable;
+
+public final class CcrStatsRequest implements Validatable {
+}

+ 62 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/CcrStatsResponse.java

@@ -0,0 +1,62 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.ccr;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+public final class CcrStatsResponse {
+
+    static final ParseField AUTO_FOLLOW_STATS_FIELD = new ParseField("auto_follow_stats");
+    static final ParseField FOLLOW_STATS_FIELD = new ParseField("follow_stats");
+
+    private static final ConstructingObjectParser<CcrStatsResponse, Void> PARSER = new ConstructingObjectParser<>("indices",
+        args -> {
+            AutoFollowStats autoFollowStats = (AutoFollowStats) args[0];
+            IndicesFollowStats indicesFollowStats = (IndicesFollowStats) args[1];
+            return new CcrStatsResponse(autoFollowStats, indicesFollowStats);
+        });
+
+    static {
+        PARSER.declareObject(ConstructingObjectParser.constructorArg(), AutoFollowStats.STATS_PARSER, AUTO_FOLLOW_STATS_FIELD);
+        PARSER.declareObject(ConstructingObjectParser.constructorArg(), IndicesFollowStats.PARSER, FOLLOW_STATS_FIELD);
+    }
+
+    public static CcrStatsResponse fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    private final AutoFollowStats autoFollowStats;
+    private final IndicesFollowStats indicesFollowStats;
+
+    public CcrStatsResponse(AutoFollowStats autoFollowStats, IndicesFollowStats indicesFollowStats) {
+        this.autoFollowStats = autoFollowStats;
+        this.indicesFollowStats = indicesFollowStats;
+    }
+
+    public AutoFollowStats getAutoFollowStats() {
+        return autoFollowStats;
+    }
+
+    public IndicesFollowStats getIndicesFollowStats() {
+        return indicesFollowStats;
+    }
+}

+ 403 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/IndicesFollowStats.java

@@ -0,0 +1,403 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.ccr;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+public final class IndicesFollowStats {
+
+    static final ParseField INDICES_FIELD = new ParseField("indices");
+    static final ParseField INDEX_FIELD = new ParseField("index");
+    static final ParseField SHARDS_FIELD = new ParseField("shards");
+
+    private static final ConstructingObjectParser<Tuple<String, List<ShardFollowStats>>, Void> ENTRY_PARSER =
+        new ConstructingObjectParser<>(
+            "entry",
+            args -> {
+                String index = (String) args[0];
+                @SuppressWarnings("unchecked")
+                List<ShardFollowStats> shardFollowStats = (List<ShardFollowStats>) args[1];
+                return new Tuple<>(index, shardFollowStats);
+            }
+        );
+
+    static {
+        ENTRY_PARSER.declareString(ConstructingObjectParser.constructorArg(), INDEX_FIELD);
+        ENTRY_PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), ShardFollowStats.PARSER, SHARDS_FIELD);
+    }
+
+    static final ConstructingObjectParser<IndicesFollowStats, Void> PARSER = new ConstructingObjectParser<>("indices",
+        args -> {
+            @SuppressWarnings("unchecked")
+            List<Tuple<String, List<ShardFollowStats>>> entries = (List<Tuple<String, List<ShardFollowStats>>>) args[0];
+            Map<String, List<ShardFollowStats>> shardFollowStats = entries.stream().collect(Collectors.toMap(Tuple::v1, Tuple::v2));
+            return new IndicesFollowStats(new TreeMap<>(shardFollowStats));
+        });
+
+    static {
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), ENTRY_PARSER, INDICES_FIELD);
+    }
+
+    private final NavigableMap<String, List<ShardFollowStats>> shardFollowStats;
+
+    IndicesFollowStats(NavigableMap<String, List<ShardFollowStats>> shardFollowStats) {
+        this.shardFollowStats = Collections.unmodifiableNavigableMap(shardFollowStats);
+    }
+
+    public List<ShardFollowStats> getShardFollowStats(String index) {
+        return shardFollowStats.get(index);
+    }
+
+    public Map<String, List<ShardFollowStats>> getShardFollowStats() {
+        return shardFollowStats;
+    }
+
+    public static final class ShardFollowStats {
+
+
+        static final ParseField LEADER_CLUSTER = new ParseField("remote_cluster");
+        static final ParseField LEADER_INDEX = new ParseField("leader_index");
+        static final ParseField FOLLOWER_INDEX = new ParseField("follower_index");
+        static final ParseField SHARD_ID = new ParseField("shard_id");
+        static final ParseField LEADER_GLOBAL_CHECKPOINT_FIELD = new ParseField("leader_global_checkpoint");
+        static final ParseField LEADER_MAX_SEQ_NO_FIELD = new ParseField("leader_max_seq_no");
+        static final ParseField FOLLOWER_GLOBAL_CHECKPOINT_FIELD = new ParseField("follower_global_checkpoint");
+        static final ParseField FOLLOWER_MAX_SEQ_NO_FIELD = new ParseField("follower_max_seq_no");
+        static final ParseField LAST_REQUESTED_SEQ_NO_FIELD = new ParseField("last_requested_seq_no");
+        static final ParseField OUTSTANDING_READ_REQUESTS = new ParseField("outstanding_read_requests");
+        static final ParseField OUTSTANDING_WRITE_REQUESTS = new ParseField("outstanding_write_requests");
+        static final ParseField WRITE_BUFFER_OPERATION_COUNT_FIELD = new ParseField("write_buffer_operation_count");
+        static final ParseField WRITE_BUFFER_SIZE_IN_BYTES_FIELD = new ParseField("write_buffer_size_in_bytes");
+        static final ParseField FOLLOWER_MAPPING_VERSION_FIELD = new ParseField("follower_mapping_version");
+        static final ParseField FOLLOWER_SETTINGS_VERSION_FIELD = new ParseField("follower_settings_version");
+        static final ParseField TOTAL_READ_TIME_MILLIS_FIELD = new ParseField("total_read_time_millis");
+        static final ParseField TOTAL_READ_REMOTE_EXEC_TIME_MILLIS_FIELD = new ParseField("total_read_remote_exec_time_millis");
+        static final ParseField SUCCESSFUL_READ_REQUESTS_FIELD = new ParseField("successful_read_requests");
+        static final ParseField FAILED_READ_REQUESTS_FIELD = new ParseField("failed_read_requests");
+        static final ParseField OPERATIONS_READ_FIELD = new ParseField("operations_read");
+        static final ParseField BYTES_READ = new ParseField("bytes_read");
+        static final ParseField TOTAL_WRITE_TIME_MILLIS_FIELD = new ParseField("total_write_time_millis");
+        static final ParseField SUCCESSFUL_WRITE_REQUESTS_FIELD = new ParseField("successful_write_requests");
+        static final ParseField FAILED_WRITE_REQUEST_FIELD = new ParseField("failed_write_requests");
+        static final ParseField OPERATIONS_WRITTEN = new ParseField("operations_written");
+        static final ParseField READ_EXCEPTIONS = new ParseField("read_exceptions");
+        static final ParseField TIME_SINCE_LAST_READ_MILLIS_FIELD = new ParseField("time_since_last_read_millis");
+        static final ParseField FATAL_EXCEPTION = new ParseField("fatal_exception");
+
+        @SuppressWarnings("unchecked")
+        static final ConstructingObjectParser<ShardFollowStats, Void> PARSER =
+            new ConstructingObjectParser<>(
+                "shard-follow-stats",
+                args -> new ShardFollowStats(
+                    (String) args[0],
+                    (String) args[1],
+                    (String) args[2],
+                    (int) args[3],
+                    (long) args[4],
+                    (long) args[5],
+                    (long) args[6],
+                    (long) args[7],
+                    (long) args[8],
+                    (int) args[9],
+                    (int) args[10],
+                    (int) args[11],
+                    (long) args[12],
+                    (long) args[13],
+                    (long) args[14],
+                    (long) args[15],
+                    (long) args[16],
+                    (long) args[17],
+                    (long) args[18],
+                    (long) args[19],
+                    (long) args[20],
+                    (long) args[21],
+                    (long) args[22],
+                    (long) args[23],
+                    (long) args[24],
+                    (long) args[25],
+                    new TreeMap<>(
+                        ((List<Map.Entry<Long, Tuple<Integer, ElasticsearchException>>>) args[26])
+                            .stream()
+                            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))),
+                    (ElasticsearchException) args[27]));
+
+        static final ConstructingObjectParser<Map.Entry<Long, Tuple<Integer, ElasticsearchException>>, Void> READ_EXCEPTIONS_ENTRY_PARSER =
+            new ConstructingObjectParser<>(
+                "shard-follow-stats-read-exceptions-entry",
+                args -> new AbstractMap.SimpleEntry<>((long) args[0], Tuple.tuple((Integer) args[1], (ElasticsearchException)args[2])));
+
+        static {
+            PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_CLUSTER);
+            PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_INDEX);
+            PARSER.declareString(ConstructingObjectParser.constructorArg(), FOLLOWER_INDEX);
+            PARSER.declareInt(ConstructingObjectParser.constructorArg(), SHARD_ID);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_GLOBAL_CHECKPOINT_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_MAX_SEQ_NO_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_GLOBAL_CHECKPOINT_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_MAX_SEQ_NO_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), LAST_REQUESTED_SEQ_NO_FIELD);
+            PARSER.declareInt(ConstructingObjectParser.constructorArg(), OUTSTANDING_READ_REQUESTS);
+            PARSER.declareInt(ConstructingObjectParser.constructorArg(), OUTSTANDING_WRITE_REQUESTS);
+            PARSER.declareInt(ConstructingObjectParser.constructorArg(), WRITE_BUFFER_OPERATION_COUNT_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), WRITE_BUFFER_SIZE_IN_BYTES_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_MAPPING_VERSION_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_SETTINGS_VERSION_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_READ_TIME_MILLIS_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_READ_REMOTE_EXEC_TIME_MILLIS_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), SUCCESSFUL_READ_REQUESTS_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), FAILED_READ_REQUESTS_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), OPERATIONS_READ_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), BYTES_READ);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_WRITE_TIME_MILLIS_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), SUCCESSFUL_WRITE_REQUESTS_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), FAILED_WRITE_REQUEST_FIELD);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), OPERATIONS_WRITTEN);
+            PARSER.declareLong(ConstructingObjectParser.constructorArg(), TIME_SINCE_LAST_READ_MILLIS_FIELD);
+            PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), READ_EXCEPTIONS_ENTRY_PARSER, READ_EXCEPTIONS);
+            PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(),
+                (p, c) -> ElasticsearchException.fromXContent(p),
+                FATAL_EXCEPTION);
+        }
+
+        static final ParseField READ_EXCEPTIONS_ENTRY_FROM_SEQ_NO = new ParseField("from_seq_no");
+        static final ParseField READ_EXCEPTIONS_RETRIES = new ParseField("retries");
+        static final ParseField READ_EXCEPTIONS_ENTRY_EXCEPTION = new ParseField("exception");
+
+        static {
+            READ_EXCEPTIONS_ENTRY_PARSER.declareLong(ConstructingObjectParser.constructorArg(), READ_EXCEPTIONS_ENTRY_FROM_SEQ_NO);
+            READ_EXCEPTIONS_ENTRY_PARSER.declareInt(ConstructingObjectParser.constructorArg(), READ_EXCEPTIONS_RETRIES);
+            READ_EXCEPTIONS_ENTRY_PARSER.declareObject(
+                ConstructingObjectParser.constructorArg(),
+                (p, c) -> ElasticsearchException.fromXContent(p),
+                READ_EXCEPTIONS_ENTRY_EXCEPTION);
+        }
+
+        private final String remoteCluster;
+        private final String leaderIndex;
+        private final String followerIndex;
+        private final int shardId;
+        private final long leaderGlobalCheckpoint;
+        private final long leaderMaxSeqNo;
+        private final long followerGlobalCheckpoint;
+        private final long followerMaxSeqNo;
+        private final long lastRequestedSeqNo;
+        private final int outstandingReadRequests;
+        private final int outstandingWriteRequests;
+        private final int writeBufferOperationCount;
+        private final long writeBufferSizeInBytes;
+        private final long followerMappingVersion;
+        private final long followerSettingsVersion;
+        private final long totalReadTimeMillis;
+        private final long totalReadRemoteExecTimeMillis;
+        private final long successfulReadRequests;
+        private final long failedReadRequests;
+        private final long operationsReads;
+        private final long bytesRead;
+        private final long totalWriteTimeMillis;
+        private final long successfulWriteRequests;
+        private final long failedWriteRequests;
+        private final long operationWritten;
+        private final long timeSinceLastReadMillis;
+        private final NavigableMap<Long, Tuple<Integer, ElasticsearchException>> readExceptions;
+        private final ElasticsearchException fatalException;
+
+        ShardFollowStats(String remoteCluster,
+                         String leaderIndex,
+                         String followerIndex,
+                         int shardId,
+                         long leaderGlobalCheckpoint,
+                         long leaderMaxSeqNo,
+                         long followerGlobalCheckpoint,
+                         long followerMaxSeqNo,
+                         long lastRequestedSeqNo,
+                         int outstandingReadRequests,
+                         int outstandingWriteRequests,
+                         int writeBufferOperationCount,
+                         long writeBufferSizeInBytes,
+                         long followerMappingVersion,
+                         long followerSettingsVersion,
+                         long totalReadTimeMillis,
+                         long totalReadRemoteExecTimeMillis,
+                         long successfulReadRequests,
+                         long failedReadRequests,
+                         long operationsReads,
+                         long bytesRead,
+                         long totalWriteTimeMillis,
+                         long successfulWriteRequests,
+                         long failedWriteRequests,
+                         long operationWritten,
+                         long timeSinceLastReadMillis,
+                         NavigableMap<Long, Tuple<Integer, ElasticsearchException>> readExceptions,
+                         ElasticsearchException fatalException) {
+            this.remoteCluster = remoteCluster;
+            this.leaderIndex = leaderIndex;
+            this.followerIndex = followerIndex;
+            this.shardId = shardId;
+            this.leaderGlobalCheckpoint = leaderGlobalCheckpoint;
+            this.leaderMaxSeqNo = leaderMaxSeqNo;
+            this.followerGlobalCheckpoint = followerGlobalCheckpoint;
+            this.followerMaxSeqNo = followerMaxSeqNo;
+            this.lastRequestedSeqNo = lastRequestedSeqNo;
+            this.outstandingReadRequests = outstandingReadRequests;
+            this.outstandingWriteRequests = outstandingWriteRequests;
+            this.writeBufferOperationCount = writeBufferOperationCount;
+            this.writeBufferSizeInBytes = writeBufferSizeInBytes;
+            this.followerMappingVersion = followerMappingVersion;
+            this.followerSettingsVersion = followerSettingsVersion;
+            this.totalReadTimeMillis = totalReadTimeMillis;
+            this.totalReadRemoteExecTimeMillis = totalReadRemoteExecTimeMillis;
+            this.successfulReadRequests = successfulReadRequests;
+            this.failedReadRequests = failedReadRequests;
+            this.operationsReads = operationsReads;
+            this.bytesRead = bytesRead;
+            this.totalWriteTimeMillis = totalWriteTimeMillis;
+            this.successfulWriteRequests = successfulWriteRequests;
+            this.failedWriteRequests = failedWriteRequests;
+            this.operationWritten = operationWritten;
+            this.timeSinceLastReadMillis = timeSinceLastReadMillis;
+            this.readExceptions = readExceptions;
+            this.fatalException = fatalException;
+        }
+
+        public String getRemoteCluster() {
+            return remoteCluster;
+        }
+
+        public String getLeaderIndex() {
+            return leaderIndex;
+        }
+
+        public String getFollowerIndex() {
+            return followerIndex;
+        }
+
+        public int getShardId() {
+            return shardId;
+        }
+
+        public long getLeaderGlobalCheckpoint() {
+            return leaderGlobalCheckpoint;
+        }
+
+        public long getLeaderMaxSeqNo() {
+            return leaderMaxSeqNo;
+        }
+
+        public long getFollowerGlobalCheckpoint() {
+            return followerGlobalCheckpoint;
+        }
+
+        public long getFollowerMaxSeqNo() {
+            return followerMaxSeqNo;
+        }
+
+        public long getLastRequestedSeqNo() {
+            return lastRequestedSeqNo;
+        }
+
+        public int getOutstandingReadRequests() {
+            return outstandingReadRequests;
+        }
+
+        public int getOutstandingWriteRequests() {
+            return outstandingWriteRequests;
+        }
+
+        public int getWriteBufferOperationCount() {
+            return writeBufferOperationCount;
+        }
+
+        public long getWriteBufferSizeInBytes() {
+            return writeBufferSizeInBytes;
+        }
+
+        public long getFollowerMappingVersion() {
+            return followerMappingVersion;
+        }
+
+        public long getFollowerSettingsVersion() {
+            return followerSettingsVersion;
+        }
+
+        public long getTotalReadTimeMillis() {
+            return totalReadTimeMillis;
+        }
+
+        public long getTotalReadRemoteExecTimeMillis() {
+            return totalReadRemoteExecTimeMillis;
+        }
+
+        public long getSuccessfulReadRequests() {
+            return successfulReadRequests;
+        }
+
+        public long getFailedReadRequests() {
+            return failedReadRequests;
+        }
+
+        public long getOperationsReads() {
+            return operationsReads;
+        }
+
+        public long getBytesRead() {
+            return bytesRead;
+        }
+
+        public long getTotalWriteTimeMillis() {
+            return totalWriteTimeMillis;
+        }
+
+        public long getSuccessfulWriteRequests() {
+            return successfulWriteRequests;
+        }
+
+        public long getFailedWriteRequests() {
+            return failedWriteRequests;
+        }
+
+        public long getOperationWritten() {
+            return operationWritten;
+        }
+
+        public long getTimeSinceLastReadMillis() {
+            return timeSinceLastReadMillis;
+        }
+
+        public NavigableMap<Long, Tuple<Integer, ElasticsearchException>> getReadExceptions() {
+            return readExceptions;
+        }
+
+        public ElasticsearchException getFatalException() {
+            return fatalException;
+        }
+    }
+
+}

+ 27 - 9
client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java

@@ -29,9 +29,12 @@ import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.support.WriteRequest;
+import org.elasticsearch.client.ccr.CcrStatsRequest;
+import org.elasticsearch.client.ccr.CcrStatsResponse;
 import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse;
+import org.elasticsearch.client.ccr.IndicesFollowStats.ShardFollowStats;
 import org.elasticsearch.client.ccr.PauseFollowRequest;
 import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.PutFollowRequest;
@@ -39,7 +42,6 @@ import org.elasticsearch.client.ccr.PutFollowResponse;
 import org.elasticsearch.client.ccr.ResumeFollowRequest;
 import org.elasticsearch.client.ccr.UnfollowRequest;
 import org.elasticsearch.client.core.AcknowledgedResponse;
-import org.elasticsearch.common.xcontent.ObjectPath;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.common.xcontent.json.JsonXContent;
@@ -47,6 +49,7 @@ import org.junit.Before;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import static org.hamcrest.Matchers.equalTo;
@@ -104,6 +107,15 @@ public class CCRIT extends ESRestHighLevelClientTestCase {
         assertThat(leaderSearchResponse.getHits().getTotalHits(), equalTo(1L));
 
         assertBusy(() -> {
+            CcrStatsRequest ccrStatsRequest = new CcrStatsRequest();
+            CcrStatsResponse ccrStatsResponse = execute(ccrStatsRequest, ccrClient::getCcrStats, ccrClient::getCcrStatsAsync);
+            List<ShardFollowStats> shardFollowStats = ccrStatsResponse.getIndicesFollowStats().getShardFollowStats("follower");
+            long followerGlobalCheckpoint = shardFollowStats.stream()
+                .mapToLong(ShardFollowStats::getFollowerGlobalCheckpoint)
+                .max()
+                .getAsLong();
+            assertThat(followerGlobalCheckpoint, equalTo(0L));
+
             SearchRequest followerSearchRequest = new SearchRequest("follower");
             SearchResponse followerSearchResponse = highLevelClient().search(followerSearchRequest, RequestOptions.DEFAULT);
             assertThat(followerSearchResponse.getHits().getTotalHits(), equalTo(1L));
@@ -120,6 +132,15 @@ public class CCRIT extends ESRestHighLevelClientTestCase {
         assertThat(resumeFollowResponse.isAcknowledged(), is(true));
 
         assertBusy(() -> {
+            CcrStatsRequest ccrStatsRequest = new CcrStatsRequest();
+            CcrStatsResponse ccrStatsResponse = execute(ccrStatsRequest, ccrClient::getCcrStats, ccrClient::getCcrStatsAsync);
+            List<ShardFollowStats> shardFollowStats = ccrStatsResponse.getIndicesFollowStats().getShardFollowStats("follower");
+            long followerGlobalCheckpoint = shardFollowStats.stream()
+                .mapToLong(ShardFollowStats::getFollowerGlobalCheckpoint)
+                .max()
+                .getAsLong();
+            assertThat(followerGlobalCheckpoint, equalTo(1L));
+
             SearchRequest followerSearchRequest = new SearchRequest("follower");
             SearchResponse followerSearchResponse = highLevelClient().search(followerSearchRequest, RequestOptions.DEFAULT);
             assertThat(followerSearchResponse.getHits().getTotalHits(), equalTo(2L));
@@ -156,15 +177,12 @@ public class CCRIT extends ESRestHighLevelClientTestCase {
         assertThat(response.isAcknowledged(), is(true));
 
         assertBusy(() -> {
-            assertThat(indexExists("copy-logs-20200101"), is(true));
-            // TODO: replace with HLRC follow stats when available:
-            Map<String, Object> rsp = toMap(client().performRequest(new Request("GET", "/copy-logs-20200101/_ccr/stats")));
-            String index = null;
-            try {
-                index = ObjectPath.eval("indices.0.index", rsp);
-            } catch (Exception e){ }
-            assertThat(index, equalTo("copy-logs-20200101"));
+            CcrStatsRequest ccrStatsRequest = new CcrStatsRequest();
+            CcrStatsResponse ccrStatsResponse = execute(ccrStatsRequest, ccrClient::getCcrStats, ccrClient::getCcrStatsAsync);
+            assertThat(ccrStatsResponse.getAutoFollowStats().getNumberOfSuccessfulFollowIndices(), equalTo(1L));
+            assertThat(ccrStatsResponse.getIndicesFollowStats().getShardFollowStats("copy-logs-20200101"), notNullValue());
         });
+        assertThat(indexExists("copy-logs-20200101"), is(true));
 
         GetAutoFollowPatternRequest getAutoFollowPatternRequest =
             randomBoolean() ? new GetAutoFollowPatternRequest("pattern1") : new GetAutoFollowPatternRequest();

+ 376 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/CcrStatsResponseTests.java

@@ -0,0 +1,376 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.ccr;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.client.ccr.IndicesFollowStats.ShardFollowStats;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.unit.ByteSizeUnit;
+import org.elasticsearch.common.unit.ByteSizeValue;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+
+public class CcrStatsResponseTests extends ESTestCase {
+
+    public void testFromXContent() throws IOException {
+        xContentTester(this::createParser,
+            CcrStatsResponseTests::createTestInstance,
+            CcrStatsResponseTests::toXContent,
+            CcrStatsResponse::fromXContent)
+            .supportsUnknownFields(false)
+            .assertEqualsConsumer(CcrStatsResponseTests::assertEqualInstances)
+            .assertToXContentEquivalence(false)
+            .test();
+    }
+
+    // Needed, because exceptions in IndicesFollowStats and AutoFollowStats cannot be compared
+    private static void assertEqualInstances(CcrStatsResponse expectedInstance, CcrStatsResponse newInstance) {
+        assertNotSame(expectedInstance, newInstance);
+
+        {
+            AutoFollowStats newAutoFollowStats = newInstance.getAutoFollowStats();
+            AutoFollowStats expectedAutoFollowStats = expectedInstance.getAutoFollowStats();
+            assertThat(newAutoFollowStats.getNumberOfSuccessfulFollowIndices(),
+                equalTo(expectedAutoFollowStats.getNumberOfSuccessfulFollowIndices()));
+            assertThat(newAutoFollowStats.getNumberOfFailedRemoteClusterStateRequests(),
+                equalTo(expectedAutoFollowStats.getNumberOfFailedRemoteClusterStateRequests()));
+            assertThat(newAutoFollowStats.getNumberOfFailedFollowIndices(),
+                equalTo(expectedAutoFollowStats.getNumberOfFailedFollowIndices()));
+            assertThat(newAutoFollowStats.getRecentAutoFollowErrors().size(),
+                equalTo(expectedAutoFollowStats.getRecentAutoFollowErrors().size()));
+            assertThat(newAutoFollowStats.getRecentAutoFollowErrors().keySet(),
+                equalTo(expectedAutoFollowStats.getRecentAutoFollowErrors().keySet()));
+            for (final Map.Entry<String, ElasticsearchException> entry : newAutoFollowStats.getRecentAutoFollowErrors().entrySet()) {
+                // x-content loses the exception
+                final ElasticsearchException expected = expectedAutoFollowStats.getRecentAutoFollowErrors().get(entry.getKey());
+                assertThat(entry.getValue().getMessage(), containsString(expected.getMessage()));
+                assertNotNull(entry.getValue().getCause());
+                assertThat(
+                    entry.getValue().getCause(),
+                    anyOf(instanceOf(ElasticsearchException.class), instanceOf(IllegalStateException.class)));
+                assertThat(entry.getValue().getCause().getMessage(), containsString(expected.getCause().getMessage()));
+            }
+        }
+        {
+            IndicesFollowStats newIndicesFollowStats = newInstance.getIndicesFollowStats();
+            IndicesFollowStats expectedIndicesFollowStats = expectedInstance.getIndicesFollowStats();
+            assertThat(newIndicesFollowStats.getShardFollowStats().size(),
+                equalTo(expectedIndicesFollowStats.getShardFollowStats().size()));
+            assertThat(newIndicesFollowStats.getShardFollowStats().keySet(),
+                equalTo(expectedIndicesFollowStats.getShardFollowStats().keySet()));
+            for (Map.Entry<String, List<ShardFollowStats>> indexEntry : newIndicesFollowStats.getShardFollowStats().entrySet()) {
+                List<ShardFollowStats> newStats = indexEntry.getValue();
+                List<ShardFollowStats> expectedStats = expectedIndicesFollowStats.getShardFollowStats(indexEntry.getKey());
+                assertThat(newStats.size(), equalTo(expectedStats.size()));
+                for (int i = 0; i < newStats.size(); i++) {
+                    ShardFollowStats actualShardFollowStats = newStats.get(i);
+                    ShardFollowStats expectedShardFollowStats = expectedStats.get(i);
+
+                    assertThat(actualShardFollowStats.getRemoteCluster(), equalTo(expectedShardFollowStats.getRemoteCluster()));
+                    assertThat(actualShardFollowStats.getLeaderIndex(), equalTo(expectedShardFollowStats.getLeaderIndex()));
+                    assertThat(actualShardFollowStats.getFollowerIndex(), equalTo(expectedShardFollowStats.getFollowerIndex()));
+                    assertThat(actualShardFollowStats.getShardId(), equalTo(expectedShardFollowStats.getShardId()));
+                    assertThat(actualShardFollowStats.getLeaderGlobalCheckpoint(),
+                        equalTo(expectedShardFollowStats.getLeaderGlobalCheckpoint()));
+                    assertThat(actualShardFollowStats.getLeaderMaxSeqNo(), equalTo(expectedShardFollowStats.getLeaderMaxSeqNo()));
+                    assertThat(actualShardFollowStats.getFollowerGlobalCheckpoint(),
+                        equalTo(expectedShardFollowStats.getFollowerGlobalCheckpoint()));
+                    assertThat(actualShardFollowStats.getLastRequestedSeqNo(), equalTo(expectedShardFollowStats.getLastRequestedSeqNo()));
+                    assertThat(actualShardFollowStats.getOutstandingReadRequests(),
+                        equalTo(expectedShardFollowStats.getOutstandingReadRequests()));
+                    assertThat(actualShardFollowStats.getOutstandingWriteRequests(),
+                        equalTo(expectedShardFollowStats.getOutstandingWriteRequests()));
+                    assertThat(actualShardFollowStats.getWriteBufferOperationCount(),
+                        equalTo(expectedShardFollowStats.getWriteBufferOperationCount()));
+                    assertThat(actualShardFollowStats.getFollowerMappingVersion(),
+                        equalTo(expectedShardFollowStats.getFollowerMappingVersion()));
+                    assertThat(actualShardFollowStats.getFollowerSettingsVersion(),
+                        equalTo(expectedShardFollowStats.getFollowerSettingsVersion()));
+                    assertThat(actualShardFollowStats.getTotalReadTimeMillis(),
+                        equalTo(expectedShardFollowStats.getTotalReadTimeMillis()));
+                    assertThat(actualShardFollowStats.getSuccessfulReadRequests(),
+                        equalTo(expectedShardFollowStats.getSuccessfulReadRequests()));
+                    assertThat(actualShardFollowStats.getFailedReadRequests(), equalTo(expectedShardFollowStats.getFailedReadRequests()));
+                    assertThat(actualShardFollowStats.getOperationsReads(), equalTo(expectedShardFollowStats.getOperationsReads()));
+                    assertThat(actualShardFollowStats.getBytesRead(), equalTo(expectedShardFollowStats.getBytesRead()));
+                    assertThat(actualShardFollowStats.getTotalWriteTimeMillis(),
+                        equalTo(expectedShardFollowStats.getTotalWriteTimeMillis()));
+                    assertThat(actualShardFollowStats.getSuccessfulWriteRequests(),
+                        equalTo(expectedShardFollowStats.getSuccessfulWriteRequests()));
+                    assertThat(actualShardFollowStats.getFailedWriteRequests(),
+                        equalTo(expectedShardFollowStats.getFailedWriteRequests()));
+                    assertThat(actualShardFollowStats.getOperationWritten(), equalTo(expectedShardFollowStats.getOperationWritten()));
+                    assertThat(actualShardFollowStats.getReadExceptions().size(),
+                        equalTo(expectedShardFollowStats.getReadExceptions().size()));
+                    assertThat(actualShardFollowStats.getReadExceptions().keySet(),
+                        equalTo(expectedShardFollowStats.getReadExceptions().keySet()));
+                    for (final Map.Entry<Long, Tuple<Integer, ElasticsearchException>> entry :
+                        actualShardFollowStats.getReadExceptions().entrySet()) {
+                        final Tuple<Integer, ElasticsearchException> expectedTuple =
+                            expectedShardFollowStats.getReadExceptions().get(entry.getKey());
+                        assertThat(entry.getValue().v1(), equalTo(expectedTuple.v1()));
+                        // x-content loses the exception
+                        final ElasticsearchException expected = expectedTuple.v2();
+                        assertThat(entry.getValue().v2().getMessage(), containsString(expected.getMessage()));
+                        assertNotNull(entry.getValue().v2().getCause());
+                        assertThat(
+                            entry.getValue().v2().getCause(),
+                            anyOf(instanceOf(ElasticsearchException.class), instanceOf(IllegalStateException.class)));
+                        assertThat(entry.getValue().v2().getCause().getMessage(), containsString(expected.getCause().getMessage()));
+                    }
+                    assertThat(actualShardFollowStats.getTimeSinceLastReadMillis(),
+                        equalTo(expectedShardFollowStats.getTimeSinceLastReadMillis()));
+                }
+            }
+        }
+    }
+
+    private static void toXContent(CcrStatsResponse response, XContentBuilder builder) throws IOException {
+        builder.startObject();
+        {
+            AutoFollowStats autoFollowStats = response.getAutoFollowStats();
+            builder.startObject(CcrStatsResponse.AUTO_FOLLOW_STATS_FIELD.getPreferredName());
+            {
+                builder.field(AutoFollowStats.NUMBER_OF_SUCCESSFUL_INDICES_AUTO_FOLLOWED.getPreferredName(),
+                    autoFollowStats.getNumberOfSuccessfulFollowIndices());
+                builder.field(AutoFollowStats.NUMBER_OF_FAILED_REMOTE_CLUSTER_STATE_REQUESTS.getPreferredName(),
+                    autoFollowStats.getNumberOfFailedRemoteClusterStateRequests());
+                builder.field(AutoFollowStats.NUMBER_OF_FAILED_INDICES_AUTO_FOLLOWED.getPreferredName(),
+                    autoFollowStats.getNumberOfFailedFollowIndices());
+                builder.startArray(AutoFollowStats.RECENT_AUTO_FOLLOW_ERRORS.getPreferredName());
+                for (Map.Entry<String, ElasticsearchException> entry : autoFollowStats.getRecentAutoFollowErrors().entrySet()) {
+                    builder.startObject();
+                    {
+                        builder.field(AutoFollowStats.LEADER_INDEX.getPreferredName(), entry.getKey());
+                        builder.field(AutoFollowStats.AUTO_FOLLOW_EXCEPTION.getPreferredName());
+                        builder.startObject();
+                        {
+                            ElasticsearchException.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS, entry.getValue());
+                        }
+                        builder.endObject();
+                    }
+                    builder.endObject();
+                }
+                builder.endArray();
+            }
+            builder.endObject();
+
+            IndicesFollowStats indicesFollowStats = response.getIndicesFollowStats();
+            builder.startObject(CcrStatsResponse.FOLLOW_STATS_FIELD.getPreferredName());
+            {
+                builder.startArray(IndicesFollowStats.INDICES_FIELD.getPreferredName());
+                for (Map.Entry<String, List<ShardFollowStats>> indexEntry :
+                    indicesFollowStats.getShardFollowStats().entrySet()) {
+                    builder.startObject();
+                    {
+                        builder.field(IndicesFollowStats.INDEX_FIELD.getPreferredName(), indexEntry.getKey());
+                        builder.startArray(IndicesFollowStats.SHARDS_FIELD.getPreferredName());
+                        {
+                            for (ShardFollowStats stats : indexEntry.getValue()) {
+                                builder.startObject();
+                                {
+                                    builder.field(ShardFollowStats.LEADER_CLUSTER.getPreferredName(), stats.getRemoteCluster());
+                                    builder.field(ShardFollowStats.LEADER_INDEX.getPreferredName(), stats.getLeaderIndex());
+                                    builder.field(ShardFollowStats.FOLLOWER_INDEX.getPreferredName(), stats.getFollowerIndex());
+                                    builder.field(ShardFollowStats.SHARD_ID.getPreferredName(), stats.getShardId());
+                                    builder.field(ShardFollowStats.LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(),
+                                        stats.getLeaderGlobalCheckpoint());
+                                    builder.field(ShardFollowStats.LEADER_MAX_SEQ_NO_FIELD.getPreferredName(), stats.getLeaderMaxSeqNo());
+                                    builder.field(ShardFollowStats.FOLLOWER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(),
+                                        stats.getFollowerGlobalCheckpoint());
+                                    builder.field(ShardFollowStats.FOLLOWER_MAX_SEQ_NO_FIELD.getPreferredName(),
+                                        stats.getFollowerMaxSeqNo());
+                                    builder.field(ShardFollowStats.LAST_REQUESTED_SEQ_NO_FIELD.getPreferredName(),
+                                        stats.getLastRequestedSeqNo());
+                                    builder.field(ShardFollowStats.OUTSTANDING_READ_REQUESTS.getPreferredName(),
+                                        stats.getOutstandingReadRequests());
+                                    builder.field(ShardFollowStats.OUTSTANDING_WRITE_REQUESTS.getPreferredName(),
+                                        stats.getOutstandingWriteRequests());
+                                    builder.field(ShardFollowStats.WRITE_BUFFER_OPERATION_COUNT_FIELD.getPreferredName(),
+                                        stats.getWriteBufferOperationCount());
+                                    builder.humanReadableField(
+                                        ShardFollowStats.WRITE_BUFFER_SIZE_IN_BYTES_FIELD.getPreferredName(),
+                                        "write_buffer_size",
+                                        new ByteSizeValue(stats.getWriteBufferSizeInBytes()));
+                                    builder.field(ShardFollowStats.FOLLOWER_MAPPING_VERSION_FIELD.getPreferredName(),
+                                        stats.getFollowerMappingVersion());
+                                    builder.field(ShardFollowStats.FOLLOWER_SETTINGS_VERSION_FIELD.getPreferredName(),
+                                        stats.getFollowerSettingsVersion());
+                                    builder.humanReadableField(
+                                        ShardFollowStats.TOTAL_READ_TIME_MILLIS_FIELD.getPreferredName(),
+                                        "total_read_time",
+                                        new TimeValue(stats.getTotalReadTimeMillis(), TimeUnit.MILLISECONDS));
+                                    builder.humanReadableField(
+                                        ShardFollowStats.TOTAL_READ_REMOTE_EXEC_TIME_MILLIS_FIELD.getPreferredName(),
+                                        "total_read_remote_exec_time",
+                                        new TimeValue(stats.getTotalReadRemoteExecTimeMillis(), TimeUnit.MILLISECONDS));
+                                    builder.field(ShardFollowStats.SUCCESSFUL_READ_REQUESTS_FIELD.getPreferredName(),
+                                        stats.getSuccessfulReadRequests());
+                                    builder.field(ShardFollowStats.FAILED_READ_REQUESTS_FIELD.getPreferredName(),
+                                        stats.getFailedReadRequests());
+                                    builder.field(ShardFollowStats.OPERATIONS_READ_FIELD.getPreferredName(), stats.getOperationsReads());
+                                    builder.humanReadableField(
+                                        ShardFollowStats.BYTES_READ.getPreferredName(),
+                                        "total_read",
+                                        new ByteSizeValue(stats.getBytesRead(), ByteSizeUnit.BYTES));
+                                    builder.humanReadableField(
+                                        ShardFollowStats.TOTAL_WRITE_TIME_MILLIS_FIELD.getPreferredName(),
+                                        "total_write_time",
+                                        new TimeValue(stats.getTotalWriteTimeMillis(), TimeUnit.MILLISECONDS));
+                                    builder.field(ShardFollowStats.SUCCESSFUL_WRITE_REQUESTS_FIELD.getPreferredName(),
+                                        stats.getSuccessfulWriteRequests());
+                                    builder.field(ShardFollowStats.FAILED_WRITE_REQUEST_FIELD.getPreferredName(),
+                                        stats.getFailedWriteRequests());
+                                    builder.field(ShardFollowStats.OPERATIONS_WRITTEN.getPreferredName(), stats.getOperationWritten());
+                                    builder.startArray(ShardFollowStats.READ_EXCEPTIONS.getPreferredName());
+                                    {
+                                        for (final Map.Entry<Long, Tuple<Integer, ElasticsearchException>> entry :
+                                            stats.getReadExceptions().entrySet()) {
+                                            builder.startObject();
+                                            {
+                                                builder.field(ShardFollowStats.READ_EXCEPTIONS_ENTRY_FROM_SEQ_NO.getPreferredName(),
+                                                    entry.getKey());
+                                                builder.field(ShardFollowStats.READ_EXCEPTIONS_RETRIES.getPreferredName(),
+                                                    entry.getValue().v1());
+                                                builder.field(ShardFollowStats.READ_EXCEPTIONS_ENTRY_EXCEPTION.getPreferredName());
+                                                builder.startObject();
+                                                {
+                                                    ElasticsearchException.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS,
+                                                        entry.getValue().v2());
+                                                }
+                                                builder.endObject();
+                                            }
+                                            builder.endObject();
+                                        }
+                                    }
+                                    builder.endArray();
+                                    builder.humanReadableField(
+                                        ShardFollowStats.TIME_SINCE_LAST_READ_MILLIS_FIELD.getPreferredName(),
+                                        "time_since_last_read",
+                                        new TimeValue(stats.getTimeSinceLastReadMillis(), TimeUnit.MILLISECONDS));
+                                    if (stats.getFatalException() != null) {
+                                        builder.field(ShardFollowStats.FATAL_EXCEPTION.getPreferredName());
+                                        builder.startObject();
+                                        {
+                                            ElasticsearchException.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS,
+                                                stats.getFatalException());
+                                        }
+                                        builder.endObject();
+                                    }
+                                }
+                                builder.endObject();
+                            }
+                        }
+                        builder.endArray();
+                    }
+                    builder.endObject();
+                }
+                builder.endArray();
+            }
+            builder.endObject();
+        }
+        builder.endObject();
+    }
+
+    private static CcrStatsResponse createTestInstance() {
+        return new CcrStatsResponse(randomAutoFollowStats(), randomIndicesFollowStats());
+    }
+
+    private static AutoFollowStats randomAutoFollowStats() {
+        final int count = randomIntBetween(0, 16);
+        final NavigableMap<String, ElasticsearchException> readExceptions = new TreeMap<>();
+        for (int i = 0; i < count; i++) {
+            readExceptions.put("" + i, new ElasticsearchException(new IllegalStateException("index [" + i + "]")));
+        }
+        return new AutoFollowStats(
+            randomNonNegativeLong(),
+            randomNonNegativeLong(),
+            randomNonNegativeLong(),
+            readExceptions
+        );
+    }
+
+    private static IndicesFollowStats randomIndicesFollowStats() {
+        int numIndices = randomIntBetween(0, 16);
+        NavigableMap<String, List<ShardFollowStats>> shardFollowStats = new TreeMap<>();
+        for (int i = 0; i < numIndices; i++) {
+            String index = randomAlphaOfLength(4);
+            int numShards = randomIntBetween(0, 5);
+            List<ShardFollowStats> stats = new ArrayList<>(numShards);
+            shardFollowStats.put(index, stats);
+            for (int j = 0; j < numShards; j++) {
+                final int count = randomIntBetween(0, 16);
+                final NavigableMap<Long, Tuple<Integer, ElasticsearchException>> readExceptions = new TreeMap<>();
+                for (long k = 0; k < count; k++) {
+                    readExceptions.put(k, new Tuple<>(randomIntBetween(0, Integer.MAX_VALUE),
+                        new ElasticsearchException(new IllegalStateException("index [" + k + "]"))));
+                }
+
+                stats.add(new ShardFollowStats(
+                    randomAlphaOfLength(4),
+                    randomAlphaOfLength(4),
+                    randomAlphaOfLength(4),
+                    randomInt(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomIntBetween(0, Integer.MAX_VALUE),
+                    randomIntBetween(0, Integer.MAX_VALUE),
+                    randomIntBetween(0, Integer.MAX_VALUE),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomNonNegativeLong(),
+                    randomLong(),
+                    readExceptions,
+                    randomBoolean() ? new ElasticsearchException("fatal error") : null));
+            }
+        }
+        return new IndicesFollowStats(shardFollowStats);
+    }
+
+}

+ 55 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java

@@ -33,10 +33,14 @@ import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.ccr.AutoFollowStats;
+import org.elasticsearch.client.ccr.CcrStatsRequest;
+import org.elasticsearch.client.ccr.CcrStatsResponse;
 import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse;
 import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse.Pattern;
+import org.elasticsearch.client.ccr.IndicesFollowStats;
 import org.elasticsearch.client.ccr.PauseFollowRequest;
 import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest;
 import org.elasticsearch.client.ccr.PutFollowRequest;
@@ -568,6 +572,57 @@ public class CCRDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testGetCCRStats() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        // tag::ccr-get-stats-request
+        CcrStatsRequest request =
+            new CcrStatsRequest(); // <1>
+        // end::ccr-get-stats-request
+
+        // tag::ccr-get-stats-execute
+        CcrStatsResponse response = client.ccr()
+            .getCcrStats(request, RequestOptions.DEFAULT);
+        // end::ccr-get-stats-execute
+
+        // tag::ccr-get-stats-response
+        IndicesFollowStats indicesFollowStats =
+            response.getIndicesFollowStats(); // <1>
+        AutoFollowStats autoFollowStats =
+            response.getAutoFollowStats(); // <2>
+        // end::ccr-get-stats-response
+
+        // tag::ccr-get-stats-execute-listener
+        ActionListener<CcrStatsResponse> listener =
+            new ActionListener<CcrStatsResponse>() {
+                @Override
+                public void onResponse(CcrStatsResponse response) { // <1>
+                    IndicesFollowStats indicesFollowStats =
+                        response.getIndicesFollowStats();
+                    AutoFollowStats autoFollowStats =
+                        response.getAutoFollowStats();
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+        // end::ccr-get-stats-execute-listener
+
+        // Replace the empty listener by a blocking listener in test
+        final CountDownLatch latch = new CountDownLatch(1);
+        listener = new LatchedActionListener<>(listener, latch);
+
+        // tag::ccr-get-stats-execute-async
+        client.ccr().getCcrStatsAsync(request,
+            RequestOptions.DEFAULT, listener); // <1>
+        // end::ccr-get-stats-execute-async
+
+        assertTrue(latch.await(30L, TimeUnit.SECONDS));
+    }
+
+
     static Map<String, Object> toMap(Response response) throws IOException {
         return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false);
     }

+ 37 - 0
docs/java-rest/high-level/ccr/get_stats.asciidoc

@@ -0,0 +1,37 @@
+--
+:api: ccr-get-stats
+:request: CcrStatsRequest
+:response: CcrStatsResponse
+--
+
+[id="{upid}-{api}"]
+=== Get CCR Stats API
+
+
+[id="{upid}-{api}-request"]
+==== Request
+
+The Get CCR Stats API allows you to get statistics about index following and auto following.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-request]
+--------------------------------------------------
+<1> The request accepts no parameters.
+
+[id="{upid}-{api}-response"]
+==== Response
+
+The returned +{response}+ always includes index follow statistics of all follow indices and
+auto follow statistics.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------
+<1> The follow stats of active follower indices.
+<2> The auto follow stats of the cluster that has been queried.
+
+include::../execution.asciidoc[]
+
+

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -473,6 +473,7 @@ The Java High Level REST Client supports the following CCR APIs:
 * <<{upid}-ccr-put-auto-follow-pattern>>
 * <<{upid}-ccr-delete-auto-follow-pattern>>
 * <<{upid}-ccr-get-auto-follow-pattern>>
+* <<{upid}-ccr-get-stats>>
 
 include::ccr/put_follow.asciidoc[]
 include::ccr/pause_follow.asciidoc[]
@@ -481,6 +482,7 @@ include::ccr/unfollow.asciidoc[]
 include::ccr/put_auto_follow_pattern.asciidoc[]
 include::ccr/delete_auto_follow_pattern.asciidoc[]
 include::ccr/get_auto_follow_pattern.asciidoc[]
+include::ccr/get_stats.asciidoc[]
 
 == Index Lifecycle Management APIs