浏览代码

Remove lenient stats parsing

Today when parsing a stats request, Elasticsearch silently ignores
incorrect metrics. This commit removes lenient parsing of stats requests
for the nodes stats and indices stats APIs.

Relates #21417
Jason Tedor 9 年之前
父节点
当前提交
f5ac0e5076

+ 46 - 37
core/src/main/java/org/elasticsearch/rest/BaseRestHandler.java

@@ -71,49 +71,58 @@ public abstract class BaseRestHandler extends AbstractComponent implements RestH
             request.unconsumedParams().stream().filter(p -> !responseParams().contains(p)).collect(Collectors.toCollection(TreeSet::new));
 
         // validate the non-response params
-        if (unconsumedParams.isEmpty() == false) {
-            String message = String.format(
-                Locale.ROOT,
-                "request [%s] contains unrecognized parameter%s: ",
-                request.path(),
-                unconsumedParams.size() > 1 ? "s" : "");
-            boolean first = true;
-            for (final String unconsumedParam : unconsumedParams) {
-                final LevensteinDistance ld = new LevensteinDistance();
-                final List<Tuple<Float, String>> scoredParams = new ArrayList<>();
-                final Set<String> candidateParams = new HashSet<>();
-                candidateParams.addAll(request.consumedParams());
-                candidateParams.addAll(responseParams());
-                for (final String candidateParam : candidateParams) {
-                    final float distance = ld.getDistance(unconsumedParam, candidateParam);
-                    if (distance > 0.5f) {
-                        scoredParams.add(new Tuple<>(distance, candidateParam));
-                    }
-                }
-                CollectionUtil.timSort(scoredParams, (a, b) -> {
-                    // sort by distance in reverse order, then parameter name for equal distances
-                    int compare = a.v1().compareTo(b.v1());
-                    if (compare != 0) return -compare;
-                    else return a.v2().compareTo(b.v2());
-                });
-                if (first == false) {
-                    message += ", ";
-                }
-                message += "[" + unconsumedParam + "]";
-                final List<String> keys = scoredParams.stream().map(Tuple::v2).collect(Collectors.toList());
-                if (keys.isEmpty() == false) {
-                    message += " -> did you mean " + (keys.size() == 1 ? "[" + keys.get(0) + "]": "any of " + keys.toString()) + "?";
-                }
-                first = false;
-            }
-
-            throw new IllegalArgumentException(message);
+        if (!unconsumedParams.isEmpty()) {
+            final Set<String> candidateParams = new HashSet<>();
+            candidateParams.addAll(request.consumedParams());
+            candidateParams.addAll(responseParams());
+            throw new IllegalArgumentException(unrecognized(request, unconsumedParams, candidateParams, "parameter"));
         }
 
         // execute the action
         action.accept(channel);
     }
 
+    protected final String unrecognized(
+        final RestRequest request,
+        final Set<String> invalids,
+        final Set<String> candidates,
+        final String detail) {
+        String message = String.format(
+            Locale.ROOT,
+            "request [%s] contains unrecognized %s%s: ",
+            request.path(),
+            detail,
+            invalids.size() > 1 ? "s" : "");
+        boolean first = true;
+        for (final String invalid : invalids) {
+            final LevensteinDistance ld = new LevensteinDistance();
+            final List<Tuple<Float, String>> scoredParams = new ArrayList<>();
+            for (final String candidate : candidates) {
+                final float distance = ld.getDistance(invalid, candidate);
+                if (distance > 0.5f) {
+                    scoredParams.add(new Tuple<>(distance, candidate));
+                }
+            }
+            CollectionUtil.timSort(scoredParams, (a, b) -> {
+                // sort by distance in reverse order, then parameter name for equal distances
+                int compare = a.v1().compareTo(b.v1());
+                if (compare != 0) return -compare;
+                else return a.v2().compareTo(b.v2());
+            });
+            if (first == false) {
+                message += ", ";
+            }
+            message += "[" + invalid + "]";
+            final List<String> keys = scoredParams.stream().map(Tuple::v2).collect(Collectors.toList());
+            if (keys.isEmpty() == false) {
+                message += " -> did you mean " + (keys.size() == 1 ? "[" + keys.get(0) + "]" : "any of " + keys.toString()) + "?";
+            }
+            first = false;
+        }
+
+        return message;
+    }
+
     /**
      * REST requests are handled by preparing a channel consumer that represents the execution of
      * the request against a channel.

+ 1 - 1
core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesInfoAction.java

@@ -55,7 +55,7 @@ public class RestNodesInfoAction extends BaseRestHandler {
     public RestNodesInfoAction(Settings settings, RestController controller, SettingsFilter settingsFilter) {
         super(settings);
         controller.registerHandler(GET, "/_nodes", this);
-        // this endpoint is used for metrics, not for nodeIds, like /_nodes/fs
+        // this endpoint is used for metrics, not for node IDs, like /_nodes/fs
         controller.registerHandler(GET, "/_nodes/{nodeId}", this);
         controller.registerHandler(GET, "/_nodes/{nodeId}/{metrics}", this);
         // added this endpoint to be aligned with stats

+ 89 - 17
core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesStatsAction.java

@@ -33,7 +33,13 @@ import org.elasticsearch.rest.action.RestActions.NodesResponseRestListener;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Consumer;
 
 import static org.elasticsearch.rest.RestRequest.Method.GET;
 
@@ -48,9 +54,38 @@ public class RestNodesStatsAction extends BaseRestHandler {
         controller.registerHandler(GET, "/_nodes/stats/{metric}", this);
         controller.registerHandler(GET, "/_nodes/{nodeId}/stats/{metric}", this);
 
-        controller.registerHandler(GET, "/_nodes/stats/{metric}/{indexMetric}", this);
+        controller.registerHandler(GET, "/_nodes/stats/{metric}/{index_metric}", this);
 
-        controller.registerHandler(GET, "/_nodes/{nodeId}/stats/{metric}/{indexMetric}", this);
+        controller.registerHandler(GET, "/_nodes/{nodeId}/stats/{metric}/{index_metric}", this);
+    }
+
+    static final Map<String, Consumer<NodesStatsRequest>> METRICS;
+
+    static {
+        final Map<String, Consumer<NodesStatsRequest>> metrics = new HashMap<>();
+        metrics.put("os", r -> r.os(true));
+        metrics.put("jvm", r -> r.jvm(true));
+        metrics.put("thread_pool", r -> r.threadPool(true));
+        metrics.put("fs", r -> r.fs(true));
+        metrics.put("transport", r -> r.transport(true));
+        metrics.put("http", r -> r.http(true));
+        metrics.put("indices", r -> r.indices(true));
+        metrics.put("process", r -> r.process(true));
+        metrics.put("breaker", r -> r.breaker(true));
+        metrics.put("script", r -> r.script(true));
+        metrics.put("discovery", r -> r.discovery(true));
+        metrics.put("ingest", r -> r.ingest(true));
+        METRICS = Collections.unmodifiableMap(metrics);
+    }
+
+    static final Map<String, Consumer<CommonStatsFlags>> FLAGS;
+
+    static {
+        final Map<String, Consumer<CommonStatsFlags>> flags = new HashMap<>();
+        for (final Flag flag : CommonStatsFlags.Flag.values()) {
+            flags.put(flag.getRestName(), f -> f.set(flag, true));
+        }
+        FLAGS = Collections.unmodifiableMap(flags);
     }
 
     @Override
@@ -62,35 +97,72 @@ public class RestNodesStatsAction extends BaseRestHandler {
         nodesStatsRequest.timeout(request.param("timeout"));
 
         if (metrics.size() == 1 && metrics.contains("_all")) {
+            if (request.hasParam("index_metric")) {
+                throw new IllegalArgumentException(
+                    String.format(
+                        Locale.ROOT,
+                        "request [%s] contains index metrics [%s] but all stats requested",
+                        request.path(),
+                        request.param("index_metric")));
+            }
             nodesStatsRequest.all();
             nodesStatsRequest.indices(CommonStatsFlags.ALL);
+        } else if (metrics.contains("_all")) {
+            throw new IllegalArgumentException(
+                String.format(Locale.ROOT,
+                    "request [%s] contains _all and individual metrics [%s]",
+                    request.path(),
+                    request.param("metric")));
         } else {
             nodesStatsRequest.clear();
-            nodesStatsRequest.os(metrics.contains("os"));
-            nodesStatsRequest.jvm(metrics.contains("jvm"));
-            nodesStatsRequest.threadPool(metrics.contains("thread_pool"));
-            nodesStatsRequest.fs(metrics.contains("fs"));
-            nodesStatsRequest.transport(metrics.contains("transport"));
-            nodesStatsRequest.http(metrics.contains("http"));
-            nodesStatsRequest.indices(metrics.contains("indices"));
-            nodesStatsRequest.process(metrics.contains("process"));
-            nodesStatsRequest.breaker(metrics.contains("breaker"));
-            nodesStatsRequest.script(metrics.contains("script"));
-            nodesStatsRequest.discovery(metrics.contains("discovery"));
-            nodesStatsRequest.ingest(metrics.contains("ingest"));
+
+            // use a sorted set so the unrecognized parameters appear in a reliable sorted order
+            final Set<String> invalidMetrics = new TreeSet<>();
+            for (final String metric : metrics) {
+                final Consumer<NodesStatsRequest> handler = METRICS.get(metric);
+                if (handler != null) {
+                    handler.accept(nodesStatsRequest);
+                } else {
+                    invalidMetrics.add(metric);
+                }
+            }
+
+            if (!invalidMetrics.isEmpty()) {
+                throw new IllegalArgumentException(unrecognized(request, invalidMetrics, METRICS.keySet(), "metric"));
+            }
 
             // check for index specific metrics
             if (metrics.contains("indices")) {
-                Set<String> indexMetrics = Strings.splitStringByCommaToSet(request.param("indexMetric", "_all"));
+                Set<String> indexMetrics = Strings.splitStringByCommaToSet(request.param("index_metric", "_all"));
                 if (indexMetrics.size() == 1 && indexMetrics.contains("_all")) {
                     nodesStatsRequest.indices(CommonStatsFlags.ALL);
                 } else {
                     CommonStatsFlags flags = new CommonStatsFlags();
-                    for (Flag flag : CommonStatsFlags.Flag.values()) {
-                        flags.set(flag, indexMetrics.contains(flag.getRestName()));
+                    flags.clear();
+                    // use a sorted set so the unrecognized parameters appear in a reliable sorted order
+                    final Set<String> invalidIndexMetrics = new TreeSet<>();
+                    for (final String indexMetric : indexMetrics) {
+                        final Consumer<CommonStatsFlags> handler = FLAGS.get(indexMetric);
+                        if (handler != null) {
+                            handler.accept(flags);
+                        } else {
+                            invalidIndexMetrics.add(indexMetric);
+                        }
+                    }
+
+                    if (!invalidIndexMetrics.isEmpty()) {
+                        throw new IllegalArgumentException(unrecognized(request, invalidIndexMetrics, FLAGS.keySet(), "index metric"));
                     }
+
                     nodesStatsRequest.indices(flags);
                 }
+            } else if (request.hasParam("index_metric")) {
+                throw new IllegalArgumentException(
+                    String.format(
+                        Locale.ROOT,
+                        "request [%s] contains index metrics [%s] but indices stats not requested",
+                        request.path(),
+                        request.param("index_metric")));
             }
         }
 

+ 50 - 17
core/src/main/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsAction.java

@@ -36,7 +36,13 @@ import org.elasticsearch.rest.action.RestBuilderListener;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Consumer;
 
 import static org.elasticsearch.rest.RestRequest.Method.GET;
 import static org.elasticsearch.rest.RestStatus.OK;
@@ -49,11 +55,34 @@ public class RestIndicesStatsAction extends BaseRestHandler {
         super(settings);
         controller.registerHandler(GET, "/_stats", this);
         controller.registerHandler(GET, "/_stats/{metric}", this);
-        controller.registerHandler(GET, "/_stats/{metric}/{indexMetric}", this);
         controller.registerHandler(GET, "/{index}/_stats", this);
         controller.registerHandler(GET, "/{index}/_stats/{metric}", this);
     }
 
+    static Map<String, Consumer<IndicesStatsRequest>> METRICS;
+
+    static {
+        final Map<String, Consumer<IndicesStatsRequest>> metrics = new HashMap<>();
+        metrics.put("docs", r -> r.docs(true));
+        metrics.put("store", r -> r.store(true));
+        metrics.put("indexing", r -> r.indexing(true));
+        metrics.put("search", r -> r.search(true));
+        metrics.put("suggest", r -> r.search(true));
+        metrics.put("get", r -> r.get(true));
+        metrics.put("merge", r -> r.merge(true));
+        metrics.put("refresh", r -> r.refresh(true));
+        metrics.put("flush", r -> r.flush(true));
+        metrics.put("warmer", r -> r.warmer(true));
+        metrics.put("query_cache", r -> r.queryCache(true));
+        metrics.put("segments", r -> r.segments(true));
+        metrics.put("fielddata", r -> r.fieldData(true));
+        metrics.put("completion", r -> r.completion(true));
+        metrics.put("request_cache", r -> r.requestCache(true));
+        metrics.put("recovery", r -> r.recovery(true));
+        metrics.put("translog", r -> r.translog(true));
+        METRICS = Collections.unmodifiableMap(metrics);
+    }
+
     @Override
     public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
         IndicesStatsRequest indicesStatsRequest = new IndicesStatsRequest();
@@ -65,24 +94,28 @@ public class RestIndicesStatsAction extends BaseRestHandler {
         // short cut, if no metrics have been specified in URI
         if (metrics.size() == 1 && metrics.contains("_all")) {
             indicesStatsRequest.all();
+        } else if (metrics.contains("_all")) {
+            throw new IllegalArgumentException(
+                String.format(Locale.ROOT,
+                    "request [%s] contains _all and individual metrics [%s]",
+                    request.path(),
+                    request.param("metric")));
         } else {
             indicesStatsRequest.clear();
-            indicesStatsRequest.docs(metrics.contains("docs"));
-            indicesStatsRequest.store(metrics.contains("store"));
-            indicesStatsRequest.indexing(metrics.contains("indexing"));
-            indicesStatsRequest.search(metrics.contains("search") || metrics.contains("suggest"));
-            indicesStatsRequest.get(metrics.contains("get"));
-            indicesStatsRequest.merge(metrics.contains("merge"));
-            indicesStatsRequest.refresh(metrics.contains("refresh"));
-            indicesStatsRequest.flush(metrics.contains("flush"));
-            indicesStatsRequest.warmer(metrics.contains("warmer"));
-            indicesStatsRequest.queryCache(metrics.contains("query_cache"));
-            indicesStatsRequest.segments(metrics.contains("segments"));
-            indicesStatsRequest.fieldData(metrics.contains("fielddata"));
-            indicesStatsRequest.completion(metrics.contains("completion"));
-            indicesStatsRequest.requestCache(metrics.contains("request_cache"));
-            indicesStatsRequest.recovery(metrics.contains("recovery"));
-            indicesStatsRequest.translog(metrics.contains("translog"));
+            // use a sorted set so the unrecognized parameters appear in a reliable sorted order
+            final Set<String> invalidMetrics = new TreeSet<>();
+            for (final String metric : metrics) {
+                final Consumer<IndicesStatsRequest> consumer = METRICS.get(metric);
+                if (consumer != null) {
+                    consumer.accept(indicesStatsRequest);
+                } else {
+                    invalidMetrics.add(metric);
+                }
+            }
+
+            if (!invalidMetrics.isEmpty()) {
+                throw new IllegalArgumentException(unrecognized(request, invalidMetrics, METRICS.keySet(), "metric"));
+            }
         }
 
         if (request.hasParam("groups")) {

+ 144 - 0
core/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestNodesStatsActionTests.java

@@ -0,0 +1,144 @@
+/*
+ * 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.rest.action.admin.cluster;
+
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.rest.RestController;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.rest.FakeRestRequest;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.object.HasToString.hasToString;
+import static org.mockito.Mockito.mock;
+
+public class RestNodesStatsActionTests extends ESTestCase {
+
+    private RestNodesStatsAction action;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        action = new RestNodesStatsAction(Settings.EMPTY, new RestController(Settings.EMPTY, Collections.emptySet()));
+    }
+
+    public void testUnrecognizedMetric() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        final String metric = randomAsciiOfLength(64);
+        params.put("metric", metric);
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_nodes/stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("request [/_nodes/stats] contains unrecognized metric: [" + metric + "]")));
+    }
+
+    public void testUnrecognizedMetricDidYouMean() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("metric", "os,transprot,unrecognized");
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_nodes/stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(
+            e,
+            hasToString(
+                containsString(
+                    "request [/_nodes/stats] contains unrecognized metrics: [transprot] -> did you mean [transport]?, [unrecognized]")));
+    }
+
+    public void testAllRequestWithOtherMetrics() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        final String metric = randomSubsetOf(1, RestNodesStatsAction.METRICS.keySet()).get(0);
+        params.put("metric", "_all," + metric);
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_nodes/stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("request [/_nodes/stats] contains _all and individual metrics [_all," + metric + "]")));
+    }
+
+    public void testUnrecognizedIndexMetric() {
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("metric", "indices");
+        final String indexMetric = randomAsciiOfLength(64);
+        params.put("index_metric", indexMetric);
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_nodes/stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("request [/_nodes/stats] contains unrecognized index metric: [" + indexMetric + "]")));
+    }
+
+    public void testUnrecognizedIndexMetricDidYouMean() {
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("metric", "indices");
+        params.put("index_metric", "indexing,stroe,unrecognized");
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_nodes/stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(
+            e,
+            hasToString(
+                containsString(
+                    "request [/_nodes/stats] contains unrecognized index metrics: [stroe] -> did you mean [store]?, [unrecognized]")));
+    }
+
+    public void testIndexMetricsRequestWithoutIndicesMetric() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        final Set<String> metrics = new HashSet<>(RestNodesStatsAction.METRICS.keySet());
+        metrics.remove("indices");
+        params.put("metric", randomSubsetOf(1, metrics).get(0));
+        final String indexMetric = randomSubsetOf(1, RestNodesStatsAction.FLAGS.keySet()).get(0);
+        params.put("index_metric", indexMetric);
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_nodes/stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(
+            e,
+            hasToString(
+                containsString("request [/_nodes/stats] contains index metrics [" + indexMetric + "] but indices stats not requested")));
+    }
+
+    public void testIndexMetricsRequestOnAllRequest() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("metric", "_all");
+        final String indexMetric = randomSubsetOf(1, RestNodesStatsAction.FLAGS.keySet()).get(0);
+        params.put("index_metric", indexMetric);
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_nodes/stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(
+            e,
+            hasToString(
+                containsString("request [/_nodes/stats] contains index metrics [" + indexMetric + "] but all stats requested")));
+    }
+
+}

+ 83 - 0
core/src/test/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsActionTests.java

@@ -0,0 +1,83 @@
+/*
+ * 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.rest.action.admin.indices;
+
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.rest.RestController;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.rest.FakeRestRequest;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.object.HasToString.hasToString;
+import static org.mockito.Mockito.mock;
+
+public class RestIndicesStatsActionTests extends ESTestCase {
+
+    private RestIndicesStatsAction action;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        action = new RestIndicesStatsAction(Settings.EMPTY, new RestController(Settings.EMPTY, Collections.emptySet()));
+    }
+
+    public void testUnrecognizedMetric() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        final String metric = randomAsciiOfLength(64);
+        params.put("metric", metric);
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("request [/_stats] contains unrecognized metric: [" + metric + "]")));
+    }
+
+    public void testUnrecognizedMetricDidYouMean() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        params.put("metric", "request_cache,fieldata,unrecognized");
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(
+            e,
+            hasToString(
+                containsString(
+                    "request [/_stats] contains unrecognized metrics: [fieldata] -> did you mean [fielddata]?, [unrecognized]")));
+    }
+
+    public void testAllRequestWithOtherMetrics() throws IOException {
+        final HashMap<String, String> params = new HashMap<>();
+        final String metric = randomSubsetOf(1, RestIndicesStatsAction.METRICS.keySet()).get(0);
+        params.put("metric", "_all," + metric);
+        final RestRequest request = new FakeRestRequest.Builder().withPath("/_stats").withParams(params).build();
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> action.prepareRequest(request, mock(NodeClient.class)));
+        assertThat(e, hasToString(containsString("request [/_stats] contains _all and individual metrics [_all," + metric + "]")));
+    }
+
+}

+ 30 - 13
docs/reference/cluster/nodes-stats.asciidoc

@@ -65,12 +65,11 @@ of `indices`, `os`, `process`, `jvm`, `transport`, `http`,
 
 [source,js]
 --------------------------------------------------
-# return indices and os
-curl -XGET 'http://localhost:9200/_nodes/stats/os'
+# return just indices
+curl -XGET 'http://localhost:9200/_nodes/stats/indices'
 # return just os and process
 curl -XGET 'http://localhost:9200/_nodes/stats/os,process'
-# specific type endpoint
-curl -XGET 'http://localhost:9200/_nodes/stats/process'
+# return just process for node with IP address 10.0.0.1
 curl -XGET 'http://localhost:9200/_nodes/10.0.0.1/stats/process'
 --------------------------------------------------
 
@@ -280,27 +279,45 @@ the current running process:
 `process.mem.total_virtual_in_bytes`::
 	Size in bytes of virtual memory that is guaranteed to be available to the running process
 
-
 [float]
-[[field-data]]
-=== Field data statistics
+[[indices-stats]]
+=== Indices statistics
 
-You can get information about field data memory usage on node
-level or on index level.
+You can get information about indices stats on node level or on index level.
 
 [source,js]
 --------------------------------------------------
-# Node Stats
-curl -XGET 'http://localhost:9200/_nodes/stats/indices/?fields=field1,field2&pretty'
+# Node level
+curl -XGET 'http://localhost:9200/_nodes/stats/indices/fielddata?fields=field1,field2&pretty'
 
-# Indices Stat
+# Index level
 curl -XGET 'http://localhost:9200/_stats/fielddata/?fields=field1,field2&pretty'
 
 # You can use wildcards for field names
+curl -XGET 'http://localhost:9200/_nodes/stats/indices/fielddata?fields=field*&pretty'
 curl -XGET 'http://localhost:9200/_stats/fielddata/?fields=field*&pretty'
-curl -XGET 'http://localhost:9200/_nodes/stats/indices/?fields=field*&pretty'
 --------------------------------------------------
 
+Supported metrics are:
+
+* `completion`
+* `docs`
+* `fielddata`
+* `flush`
+* `get`
+* `indexing`
+* `merge`
+* `query_cache`
+* `recovery`
+* `refresh`
+* `request_cache`
+* `search`
+* `segments`
+* `store`
+* `suggest`
+* `translog`
+* `warmer`
+
 [float]
 [[search-groups]]
 === Search groups

+ 1 - 1
docs/reference/indices/flush.asciidoc

@@ -74,7 +74,7 @@ the <<indices-stats,indices stats>> API:
 
 [source,sh]
 --------------------------------------------------
-GET twitter/_stats/commit?level=shards
+GET twitter/_stats?level=shards
 --------------------------------------------------
 // CONSOLE
 // TEST[s/^/PUT twitter\n/]

+ 14 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/indices.stats/10_index.yaml

@@ -100,3 +100,17 @@ setup:
   - is_false: indices.test1
   - is_true: indices.test2
 
+---
+"Indices stats unrecognized parameter":
+  - skip:
+      version: " - 5.99.99"
+      reason: awaits strict stats handling to be backported to 5.x
+  - do:
+      indices.stats:
+        metric: [ fieldata ]
+        ignore: 400
+
+  - match: { status: 400 }
+  - match: { error.type: illegal_argument_exception }
+  - match: { error.reason: "request [/_stats/fieldata] contains unrecognized metric: [fieldata] -> did you mean [fielddata]?" }
+

+ 14 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/nodes.stats/10_basic.yaml

@@ -20,3 +20,17 @@
         level: "indices"
 
   - is_true: nodes.$master.indices.indices
+
+---
+"Nodes stats unrecognized parameter":
+  - skip:
+      version: " - 5.99.99"
+      reason: awaits strict stats handling to be backported to 5.x
+  - do:
+      nodes.stats:
+        metric: [ transprot ]
+        ignore: 400
+
+  - match: { status: 400 }
+  - match: { error.type: illegal_argument_exception }
+  - match: { error.reason: "request [/_nodes/stats/transprot] contains unrecognized metric: [transprot] -> did you mean [transport]?" }