Browse Source

Add xpack usage stats for the spatial plugin (#61946)

This commit leverages the analytics plugin's aggregation usage
framework and adopts it to the spatial plugin.

This is just a skeleton, as there are no aggregations to
be tracked just yet, but future ones will be introduced and
their usage should be tracked here.
Tal Levy 5 years ago
parent
commit
abc73c9485
17 changed files with 503 additions and 34 deletions
  1. 1 1
      x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsUsage.java
  2. 1 1
      x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/action/AnalyticsStatsActionNodeResponseTests.java
  3. 1 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/analytics/action/AnalyticsStatsAction.java
  4. 1 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/stats/EnumCounters.java
  5. 21 4
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/SpatialFeatureSetUsage.java
  6. 177 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/action/SpatialStatsAction.java
  7. 1 2
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/stats/EnumCountersTests.java
  8. 21 2
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/spatial/SpatialFeatureSetUsageTests.java
  9. 1 0
      x-pack/plugin/spatial/build.gradle
  10. 6 1
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java
  11. 39 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsage.java
  12. 2 4
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/action/SpatialInfoTransportAction.java
  13. 57 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/action/SpatialStatsTransportAction.java
  14. 19 7
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/action/SpatialUsageTransportAction.java
  15. 49 10
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/action/SpatialInfoTransportActionTests.java
  16. 92 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/action/SpatialStatsTransportActionTests.java
  17. 14 0
      x-pack/plugin/spatial/src/yamlRestTest/resources/rest-api-spec/test/50_feature_usage.yml

+ 1 - 1
x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsUsage.java

@@ -8,7 +8,7 @@ package org.elasticsearch.xpack.analytics;
 
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.common.xcontent.ContextParser;
-import org.elasticsearch.xpack.core.analytics.EnumCounters;
+import org.elasticsearch.xpack.core.common.stats.EnumCounters;
 import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
 
 /**

+ 1 - 1
x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/action/AnalyticsStatsActionNodeResponseTests.java

@@ -10,7 +10,7 @@ import org.elasticsearch.Version;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.test.AbstractWireSerializingTestCase;
-import org.elasticsearch.xpack.core.analytics.EnumCounters;
+import org.elasticsearch.xpack.core.common.stats.EnumCounters;
 import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
 
 import static org.hamcrest.Matchers.equalTo;

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/analytics/action/AnalyticsStatsAction.java

@@ -19,7 +19,7 @@ import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.transport.TransportRequest;
-import org.elasticsearch.xpack.core.analytics.EnumCounters;
+import org.elasticsearch.xpack.core.common.stats.EnumCounters;
 
 import java.io.IOException;
 import java.util.List;

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/analytics/EnumCounters.java → x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/stats/EnumCounters.java

@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-package org.elasticsearch.xpack.core.analytics;
+package org.elasticsearch.xpack.core.common.stats;
 
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;

+ 21 - 4
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/SpatialFeatureSetUsage.java

@@ -11,18 +11,27 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.xpack.core.XPackFeatureSet;
 import org.elasticsearch.xpack.core.XPackField;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
 
 import java.io.IOException;
 import java.util.Objects;
 
 public class SpatialFeatureSetUsage extends XPackFeatureSet.Usage {
 
-    public SpatialFeatureSetUsage(boolean available, boolean enabled) {
+    private final SpatialStatsAction.Response statsResponse;
+
+    public SpatialFeatureSetUsage(boolean available, boolean enabled, SpatialStatsAction.Response statsResponse) {
         super(XPackField.SPATIAL, available, enabled);
+        this.statsResponse = statsResponse;
     }
 
     public SpatialFeatureSetUsage(StreamInput input) throws IOException {
         super(input);
+        if (input.getVersion().onOrAfter(Version.V_8_0_0)) {
+            this.statsResponse = new SpatialStatsAction.Response(input);
+        } else {
+            this.statsResponse = null;
+        }
     }
 
     @Override
@@ -30,14 +39,21 @@ public class SpatialFeatureSetUsage extends XPackFeatureSet.Usage {
         return Version.V_7_4_0;
     }
 
+    SpatialStatsAction.Response statsResponse() {
+        return statsResponse;
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         super.writeTo(out);
+        if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
+            this.statsResponse.writeTo(out);
+        }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(available, enabled);
+        return Objects.hash(available, enabled, statsResponse);
     }
 
     @Override
@@ -49,7 +65,8 @@ public class SpatialFeatureSetUsage extends XPackFeatureSet.Usage {
             return false;
         }
         SpatialFeatureSetUsage other = (SpatialFeatureSetUsage) obj;
-        return Objects.equals(available, other.available) &&
-            Objects.equals(enabled, other.enabled);
+        return Objects.equals(available, other.available)
+            && Objects.equals(enabled, other.enabled)
+            && Objects.equals(statsResponse, other.statsResponse);
     }
 }

+ 177 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/action/SpatialStatsAction.java

@@ -0,0 +1,177 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.core.spatial.action;
+
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.FailedNodeException;
+import org.elasticsearch.action.support.nodes.BaseNodeResponse;
+import org.elasticsearch.action.support.nodes.BaseNodesRequest;
+import org.elasticsearch.action.support.nodes.BaseNodesResponse;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.transport.TransportRequest;
+import org.elasticsearch.xpack.core.common.stats.EnumCounters;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class SpatialStatsAction extends ActionType<SpatialStatsAction.Response> {
+    public static final SpatialStatsAction INSTANCE = new SpatialStatsAction();
+    public static final String NAME = "cluster:monitor/xpack/spatial/stats";
+
+    private SpatialStatsAction() {
+        super(NAME, Response::new);
+    }
+
+    /**
+     * Items to track. Serialized by ordinals. Append only, don't remove or change order of items in this list.
+     */
+    public enum Item {
+    }
+
+    public static class Request extends BaseNodesRequest<Request> implements ToXContentObject {
+
+        public Request() {
+            super((String[]) null);
+        }
+
+        public Request(StreamInput in) throws IOException {
+            super(in);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.endObject();
+            return builder;
+        }
+
+        @Override
+        public int hashCode() {
+            // Nothing to hash atm, so just use the action name
+            return Objects.hashCode(NAME);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    public static class NodeRequest extends TransportRequest {
+        public NodeRequest(StreamInput in) throws IOException {
+            super(in);
+        }
+
+        public NodeRequest(Request request) {
+
+        }
+    }
+
+    public static class Response extends BaseNodesResponse<NodeResponse> implements Writeable, ToXContentObject {
+        public Response(StreamInput in) throws IOException {
+            super(in);
+        }
+
+        public Response(ClusterName clusterName, List<NodeResponse> nodes, List<FailedNodeException> failures) {
+            super(clusterName, nodes, failures);
+        }
+
+        @Override
+        protected List<NodeResponse> readNodesFrom(StreamInput in) throws IOException {
+            return in.readList(NodeResponse::new);
+        }
+
+        @Override
+        protected void writeNodesTo(StreamOutput out, List<NodeResponse> nodes) throws IOException {
+            out.writeList(nodes);
+        }
+
+        public EnumCounters<Item> getStats() {
+            List<EnumCounters<Item>> countersPerNode = getNodes()
+                .stream()
+                .map(SpatialStatsAction.NodeResponse::getStats)
+                .collect(Collectors.toList());
+            return EnumCounters.merge(Item.class, countersPerNode);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            EnumCounters<Item> stats = getStats();
+            builder.startObject("stats");
+            for (Item item : Item.values()) {
+                builder.field(item.name().toLowerCase(Locale.ROOT) + "_usage", stats.get(item));
+            }
+            builder.endObject();
+            return builder;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(getStats());
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Response other = (Response) o;
+            return Objects.equals(getStats(), other.getStats());
+        }
+    }
+
+    public static class NodeResponse extends BaseNodeResponse {
+        private final EnumCounters<Item> counters;
+
+        public NodeResponse(DiscoveryNode node, EnumCounters<Item> counters) {
+            super(node);
+            this.counters = counters;
+        }
+
+        public NodeResponse(StreamInput in) throws IOException {
+            super(in);
+            counters = new EnumCounters<>(in, Item.class);
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            super.writeTo(out);
+            counters.writeTo(out);
+        }
+
+        public EnumCounters<Item> getStats() {
+            return counters;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            NodeResponse that = (NodeResponse) o;
+            return counters.equals(that.counters) &&
+                getNode().equals(that.getNode());
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(counters, getNode());
+        }
+    }
+}

+ 1 - 2
x-pack/plugin/core/src/test/java/org/elasticsearch/analytics/EnumCountersTests.java → x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/stats/EnumCountersTests.java

@@ -4,14 +4,13 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-package org.elasticsearch.analytics;
+package org.elasticsearch.xpack.core.common.stats;
 
 import org.elasticsearch.Version;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.test.AbstractWireTestCase;
-import org.elasticsearch.xpack.core.analytics.EnumCounters;
 
 import java.io.IOException;
 import java.util.Map;

+ 21 - 2
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/spatial/SpatialFeatureSetUsageTests.java

@@ -5,10 +5,20 @@
  */
 package org.elasticsearch.xpack.core.spatial;
 
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.transport.TransportAddress;
 import org.elasticsearch.test.AbstractWireSerializingTestCase;
+import org.elasticsearch.xpack.core.common.stats.EnumCounters;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.util.List;
+
+import static java.util.Collections.emptyList;
 
 public class SpatialFeatureSetUsageTests extends AbstractWireSerializingTestCase<SpatialFeatureSetUsage> {
 
@@ -16,13 +26,15 @@ public class SpatialFeatureSetUsageTests extends AbstractWireSerializingTestCase
     protected SpatialFeatureSetUsage createTestInstance() {
         boolean available = randomBoolean();
         boolean enabled = randomBoolean();
-        return new SpatialFeatureSetUsage(available, enabled);
+        SpatialStatsAction.Response statsResponse = randomStatsResponse();
+        return new SpatialFeatureSetUsage(available, enabled, statsResponse);
     }
 
     @Override
     protected SpatialFeatureSetUsage mutateInstance(SpatialFeatureSetUsage instance) throws IOException {
         boolean available = instance.available();
         boolean enabled = instance.enabled();
+        SpatialStatsAction.Response statsResponse = instance.statsResponse();
         switch (between(0, 1)) {
             case 0:
                 available = available == false;
@@ -33,7 +45,7 @@ public class SpatialFeatureSetUsageTests extends AbstractWireSerializingTestCase
             default:
                 throw new AssertionError("Illegal randomisation branch");
         }
-        return new SpatialFeatureSetUsage(available, enabled);
+        return new SpatialFeatureSetUsage(available, enabled, statsResponse);
     }
 
     @Override
@@ -41,4 +53,11 @@ public class SpatialFeatureSetUsageTests extends AbstractWireSerializingTestCase
         return SpatialFeatureSetUsage::new;
     }
 
+    private SpatialStatsAction.Response randomStatsResponse() {
+        DiscoveryNode node = new DiscoveryNode("_node_id",
+            new TransportAddress(InetAddress.getLoopbackAddress(), 9300), Version.CURRENT);
+        EnumCounters<SpatialStatsAction.Item> counters = new EnumCounters<>(SpatialStatsAction.Item.class);
+        SpatialStatsAction.NodeResponse nodeResponse = new SpatialStatsAction.NodeResponse(node, counters);
+        return new SpatialStatsAction.Response(new ClusterName("cluster_name"), List.of(nodeResponse), emptyList());
+    }
 }

+ 1 - 0
x-pack/plugin/spatial/build.gradle

@@ -20,6 +20,7 @@ dependencies {
 restResources {
   restApi {
     includeCore '_common', 'bulk', 'indices', 'index', 'search'
+    includeXpack 'xpack'
   }
   restTests {
     includeCore 'geo_shape'

+ 6 - 1
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java

@@ -28,6 +28,10 @@ import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry;
 import org.elasticsearch.xpack.core.XPackPlugin;
 import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
 import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
+import org.elasticsearch.xpack.spatial.action.SpatialInfoTransportAction;
+import org.elasticsearch.xpack.spatial.action.SpatialStatsTransportAction;
+import org.elasticsearch.xpack.spatial.action.SpatialUsageTransportAction;
 import org.elasticsearch.xpack.spatial.aggregations.metrics.GeoShapeCentroidAggregator;
 import org.elasticsearch.xpack.spatial.index.mapper.GeoShapeWithDocValuesFieldMapper;
 import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper;
@@ -66,7 +70,8 @@ public class SpatialPlugin extends GeoPlugin implements ActionPlugin, MapperPlug
     public List<ActionPlugin.ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
         return Arrays.asList(
             new ActionPlugin.ActionHandler<>(XPackUsageFeatureAction.SPATIAL, SpatialUsageTransportAction.class),
-            new ActionPlugin.ActionHandler<>(XPackInfoFeatureAction.SPATIAL, SpatialInfoTransportAction.class));
+            new ActionPlugin.ActionHandler<>(XPackInfoFeatureAction.SPATIAL, SpatialInfoTransportAction.class),
+            new ActionPlugin.ActionHandler<>(SpatialStatsAction.INSTANCE, SpatialStatsTransportAction.class));
     }
 
     @Override

+ 39 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsage.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.spatial;
+
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.common.xcontent.ContextParser;
+import org.elasticsearch.xpack.core.common.stats.EnumCounters;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
+
+/**
+ * Tracks usage of the Spatial aggregations.
+ */
+public class SpatialUsage {
+
+    private final EnumCounters<SpatialStatsAction.Item> counters = new EnumCounters<>(SpatialStatsAction.Item.class);
+
+    public SpatialUsage() {
+    }
+
+    /**
+     * Track successful parsing.
+     */
+    public <C, T> ContextParser<C, T> track(SpatialStatsAction.Item item, ContextParser<C, T> realParser) {
+        return (parser, context) -> {
+            T value = realParser.parse(parser, context);
+            // Intentionally doesn't count unless the parser returns cleanly.
+            counters.inc(item);
+            return value;
+        };
+    }
+
+    public SpatialStatsAction.NodeResponse stats(DiscoveryNode node) {
+        return new SpatialStatsAction.NodeResponse(node, counters);
+    }
+}

+ 2 - 4
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialInfoTransportAction.java → x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/action/SpatialInfoTransportAction.java

@@ -3,11 +3,10 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-package org.elasticsearch.xpack.spatial;
+package org.elasticsearch.xpack.spatial.action;
 
 import org.elasticsearch.action.support.ActionFilters;
 import org.elasticsearch.common.inject.Inject;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.XPackField;
@@ -19,8 +18,7 @@ public class SpatialInfoTransportAction extends XPackInfoFeatureTransportAction
     private final XPackLicenseState licenseState;
 
     @Inject
-    public SpatialInfoTransportAction(TransportService transportService, ActionFilters actionFilters,
-                                      Settings settings, XPackLicenseState licenseState) {
+    public SpatialInfoTransportAction(TransportService transportService, ActionFilters actionFilters, XPackLicenseState licenseState) {
         super(XPackInfoFeatureAction.SPATIAL.name(), transportService, actionFilters);
         this.licenseState = licenseState;
     }

+ 57 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/action/SpatialStatsTransportAction.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.spatial.action;
+
+import org.elasticsearch.action.FailedNodeException;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.nodes.TransportNodesAction;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
+import org.elasticsearch.xpack.spatial.SpatialUsage;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SpatialStatsTransportAction extends TransportNodesAction<SpatialStatsAction.Request, SpatialStatsAction.Response,
+        SpatialStatsAction.NodeRequest, SpatialStatsAction.NodeResponse> {
+    private final SpatialUsage usage;
+
+    @Inject
+    public SpatialStatsTransportAction(TransportService transportService, ClusterService clusterService,
+                                       ThreadPool threadPool, ActionFilters actionFilters, SpatialUsage usage) {
+        super(SpatialStatsAction.NAME, threadPool, clusterService, transportService, actionFilters,
+            SpatialStatsAction.Request::new, SpatialStatsAction.NodeRequest::new, ThreadPool.Names.MANAGEMENT,
+            SpatialStatsAction.NodeResponse.class);
+        this.usage = usage;
+    }
+
+    @Override
+    protected SpatialStatsAction.Response newResponse(SpatialStatsAction.Request request,
+                                                        List<SpatialStatsAction.NodeResponse> nodes,
+                                                        List<FailedNodeException> failures) {
+        return new SpatialStatsAction.Response(clusterService.getClusterName(), nodes, failures);
+    }
+
+    @Override
+    protected SpatialStatsAction.NodeRequest newNodeRequest(SpatialStatsAction.Request request) {
+        return new SpatialStatsAction.NodeRequest(request);
+    }
+
+    @Override
+    protected SpatialStatsAction.NodeResponse newNodeResponse(StreamInput in) throws IOException {
+        return new SpatialStatsAction.NodeResponse(in);
+    }
+
+    @Override
+    protected SpatialStatsAction.NodeResponse nodeOperation(SpatialStatsAction.NodeRequest request, Task task) {
+        return usage.stats(clusterService.localNode());
+    }
+}

+ 19 - 7
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsageTransportAction.java → x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/action/SpatialUsageTransportAction.java

@@ -3,15 +3,15 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-package org.elasticsearch.xpack.spatial;
+package org.elasticsearch.xpack.spatial.action;
 
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.inject.Inject;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.protocol.xpack.XPackUsageRequest;
 import org.elasticsearch.tasks.Task;
@@ -21,26 +21,38 @@ import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
 import org.elasticsearch.xpack.core.action.XPackUsageFeatureResponse;
 import org.elasticsearch.xpack.core.action.XPackUsageFeatureTransportAction;
 import org.elasticsearch.xpack.core.spatial.SpatialFeatureSetUsage;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
+
+import java.util.Collections;
 
 public class SpatialUsageTransportAction extends XPackUsageFeatureTransportAction {
 
-    private final Settings settings;
     private final XPackLicenseState licenseState;
+    private final Client client;
 
     @Inject
     public SpatialUsageTransportAction(TransportService transportService, ClusterService clusterService, ThreadPool threadPool,
                                        ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
-                                       Settings settings, XPackLicenseState licenseState) {
+                                       XPackLicenseState licenseState, Client client) {
         super(XPackUsageFeatureAction.SPATIAL.name(), transportService, clusterService,
             threadPool, actionFilters, indexNameExpressionResolver);
-        this.settings = settings;
         this.licenseState = licenseState;
+        this.client = client;
     }
 
     @Override
     protected void masterOperation(Task task, XPackUsageRequest request, ClusterState state,
                                    ActionListener<XPackUsageFeatureResponse> listener) {
-        SpatialFeatureSetUsage usage = new SpatialFeatureSetUsage(licenseState.isAllowed(XPackLicenseState.Feature.SPATIAL), true);
-        listener.onResponse(new XPackUsageFeatureResponse(usage));
+        if (licenseState.isAllowed(XPackLicenseState.Feature.SPATIAL)) {
+            SpatialStatsAction.Request statsRequest = new SpatialStatsAction.Request();
+            statsRequest.setParentTask(clusterService.localNode().getId(), task.getId());
+            client.execute(SpatialStatsAction.INSTANCE, statsRequest, ActionListener.wrap(r ->
+                    listener.onResponse(new XPackUsageFeatureResponse(new SpatialFeatureSetUsage(true, true, r))),
+                listener::onFailure));
+        } else {
+            SpatialFeatureSetUsage usage = new SpatialFeatureSetUsage(false, true,
+                new SpatialStatsAction.Response(state.getClusterName(), Collections.emptyList(), Collections.emptyList()));
+            listener.onResponse(new XPackUsageFeatureResponse(usage));
+        }
     }
 }

+ 49 - 10
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/SpatialInfoTransportActionTests.java → x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/action/SpatialInfoTransportActionTests.java

@@ -3,44 +3,71 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-package org.elasticsearch.xpack.spatial;
+package org.elasticsearch.xpack.spatial.action;
 
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.support.ActionFilters;
 import org.elasticsearch.action.support.PlainActionFuture;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.tasks.Task;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.XPackFeatureSet;
 import org.elasticsearch.xpack.core.action.XPackUsageFeatureResponse;
 import org.elasticsearch.xpack.core.spatial.SpatialFeatureSetUsage;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
 import org.junit.Before;
+import org.mockito.stubbing.Answer;
+
+import java.util.Collections;
 
 import static org.hamcrest.core.Is.is;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 public class SpatialInfoTransportActionTests extends ESTestCase {
 
     private XPackLicenseState licenseState;
+    private ClusterService clusterService;
 
     @Before
     public void init() {
         licenseState = mock(XPackLicenseState.class);
+        clusterService = mock(ClusterService.class);
+
+        DiscoveryNode discoveryNode = new DiscoveryNode("nodeId", buildNewFakeTransportAddress(), Version.CURRENT);
+        when(clusterService.localNode()).thenReturn(discoveryNode);
+        ClusterName clusterName = new ClusterName("cluster_name");
+        when(clusterService.getClusterName()).thenReturn(clusterName);
+        ClusterState clusterState = mock(ClusterState.class);
+        when(clusterState.getMetadata()).thenReturn(Metadata.EMPTY_METADATA);
+        when(clusterState.getClusterName()).thenReturn(clusterName);
+        when(clusterService.state()).thenReturn(clusterState);
     }
 
     public void testAvailable() throws Exception {
         SpatialInfoTransportAction featureSet = new SpatialInfoTransportAction(
-            mock(TransportService.class), mock(ActionFilters.class), Settings.EMPTY, licenseState);
+            mock(TransportService.class), mock(ActionFilters.class), licenseState);
         boolean available = randomBoolean();
         when(licenseState.isAllowed(XPackLicenseState.Feature.SPATIAL)).thenReturn(available);
         assertThat(featureSet.available(), is(available));
 
-        var usageAction = new SpatialUsageTransportAction(mock(TransportService.class), null, null,
-            mock(ActionFilters.class), null, Settings.EMPTY, licenseState);
+        var usageAction = new SpatialUsageTransportAction(mock(TransportService.class), clusterService, null,
+            mock(ActionFilters.class), null, licenseState, mockClient());
         PlainActionFuture<XPackUsageFeatureResponse> future = new PlainActionFuture<>();
-        usageAction.masterOperation(null, null, null, future);
+        Task task = new Task(1L, "_type", "_action", "_description", null, Collections.emptyMap());
+        usageAction.masterOperation(task, null, clusterService.state(), future);
         XPackFeatureSet.Usage usage = future.get().getUsage();
         assertThat(usage.available(), is(available));
 
@@ -51,16 +78,16 @@ public class SpatialInfoTransportActionTests extends ESTestCase {
     }
 
     public void testEnabled() throws Exception {
-        Settings.Builder settings = Settings.builder();
         SpatialInfoTransportAction featureSet = new SpatialInfoTransportAction(
-            mock(TransportService.class), mock(ActionFilters.class), settings.build(), licenseState);
+            mock(TransportService.class), mock(ActionFilters.class), licenseState);
         assertThat(featureSet.enabled(), is(true));
         assertTrue(featureSet.enabled());
 
         SpatialUsageTransportAction usageAction = new SpatialUsageTransportAction(mock(TransportService.class),
-            null, null, mock(ActionFilters.class), null, settings.build(), licenseState);
+            clusterService, null, mock(ActionFilters.class), null,
+            licenseState, mockClient());
         PlainActionFuture<XPackUsageFeatureResponse> future = new PlainActionFuture<>();
-        usageAction.masterOperation(null, null, null, future);
+        usageAction.masterOperation(null, null, clusterService.state(), future);
         XPackFeatureSet.Usage usage = future.get().getUsage();
         assertTrue(usage.enabled());
 
@@ -70,4 +97,16 @@ public class SpatialInfoTransportActionTests extends ESTestCase {
         assertTrue(serializedUsage.enabled());
     }
 
+    private Client mockClient() {
+        Client client = mock(Client.class);
+        doAnswer((Answer<Void>) invocation -> {
+            @SuppressWarnings("unchecked")
+            ActionListener<SpatialStatsAction.Response> listener =
+                (ActionListener<SpatialStatsAction.Response>) invocation.getArguments()[2];
+            listener.onResponse(new SpatialStatsAction.Response(clusterService.getClusterName(),
+                Collections.emptyList(), Collections.emptyList()));
+            return null;
+        }).when(client).execute(eq(SpatialStatsAction.INSTANCE), any(), any());
+        return client;
+    }
 }

+ 92 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/action/SpatialStatsTransportActionTests.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.spatial.action;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.xcontent.ContextParser;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.rest.yaml.ObjectPath;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xpack.core.spatial.action.SpatialStatsAction;
+import org.elasticsearch.xpack.spatial.SpatialUsage;
+import org.junit.Before;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.emptyList;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SpatialStatsTransportActionTests extends ESTestCase {
+
+    private TransportService transportService;
+    private ClusterService clusterService;
+    private ThreadPool threadPool;
+
+    @Before
+    public void mockServices() {
+        transportService = mock(TransportService.class);
+        threadPool = mock(ThreadPool.class);
+        clusterService = mock(ClusterService.class);
+
+        DiscoveryNode discoveryNode = new DiscoveryNode("nodeId", buildNewFakeTransportAddress(), Version.CURRENT);
+        when(clusterService.localNode()).thenReturn(discoveryNode);
+        ClusterName clusterName = new ClusterName("cluster_name");
+        when(clusterService.getClusterName()).thenReturn(clusterName);
+        ClusterState clusterState = mock(ClusterState.class);
+        when(clusterState.getMetadata()).thenReturn(Metadata.EMPTY_METADATA);
+        when(clusterService.state()).thenReturn(clusterState);
+    }
+
+    public void testUsage() throws IOException {
+        for (SpatialStatsAction.Item item : SpatialStatsAction.Item.values()) {
+            SpatialUsage usage = new SpatialUsage();
+            ContextParser<Void, Void> parser = usage.track(item, (p, c) -> c);
+            int count = between(1, 10000);
+            for (int i = 0; i < count; i++) {
+                assertNull(parser.parse(null, null));
+            }
+            ObjectPath used = buildSpatialStatsResponse(usage);
+            assertThat(item.name(), used.evaluate("stats." + item.name().toLowerCase(Locale.ROOT) + "_usage"),equalTo(count));
+        }
+    }
+
+    private SpatialStatsTransportAction toAction(SpatialUsage nodeUsage) {
+        return new SpatialStatsTransportAction(transportService, clusterService, threadPool,
+            new ActionFilters(Collections.emptySet()), nodeUsage);
+    }
+
+    private ObjectPath buildSpatialStatsResponse(SpatialUsage... nodeUsages) throws IOException {
+        SpatialStatsAction.Request request = new SpatialStatsAction.Request();
+        List<SpatialStatsAction.NodeResponse> nodeResponses = Arrays.stream(nodeUsages)
+            .map(usage -> toAction(usage).nodeOperation(new SpatialStatsAction.NodeRequest(request), null))
+            .collect(Collectors.toList());
+        SpatialStatsAction.Response response = new SpatialStatsAction.Response(
+            new ClusterName("cluster_name"), nodeResponses, emptyList());
+        try (XContentBuilder builder = jsonBuilder()) {
+            response.toXContent(builder, ToXContent.EMPTY_PARAMS);
+            return ObjectPath.createFromXContent(JsonXContent.jsonXContent, BytesReference.bytes(builder));
+        }
+    }
+}

+ 14 - 0
x-pack/plugin/spatial/src/yamlRestTest/resources/rest-api-spec/test/50_feature_usage.yml

@@ -0,0 +1,14 @@
+---
+"Basic test of xpack info of spatial features":
+  - do:
+      xpack.info: {}
+  - match: { features.spatial.available: true }
+  - match: { features.spatial.enabled: true }
+
+---
+"Basic test of xpack usage of spatial features":
+  - do:
+      xpack.usage: {}
+  - match: { spatial.available: true }
+  - match: { spatial.available: true }
+  - match: { spatial.stats: null }