diff --git a/docs/reference/rest-api/info.asciidoc b/docs/reference/rest-api/info.asciidoc
index bfd7cad1389..586188bd5b7 100644
--- a/docs/reference/rest-api/info.asciidoc
+++ b/docs/reference/rest-api/info.asciidoc
@@ -123,6 +123,10 @@ Example response:
"available" : true,
"enabled" : true
},
+ "eql" : {
+ "available" : true,
+ "enabled" : true
+ },
"sql" : {
"available" : true,
"enabled" : true
@@ -153,6 +157,8 @@ Example response:
// TESTRESPONSE[s/"expiry_date_in_millis" : 1542665112332/"expiry_date_in_millis" : "$body.license.expiry_date_in_millis"/]
// TESTRESPONSE[s/"version" : "7.0.0-alpha1-SNAPSHOT",/"version": "$body.features.ml.native_code_info.version",/]
// TESTRESPONSE[s/"build_hash" : "99a07c016d5a73"/"build_hash": "$body.features.ml.native_code_info.build_hash"/]
+// TESTRESPONSE[s/"eql" : \{[^\}]*\},/"eql": $body.$_path,/]
+// eql is disabled by default on release builds and enabled everywhere else during the initial implementation phase until its release
// So much s/// but at least we test that the layout is close to matching....
The following example only returns the build and features information:
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java
index 4a532ccbf67..6e70cfad543 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java
@@ -706,6 +706,15 @@ public class XPackLicenseState {
return localStatus.active;
}
+ /**
+ * Determine if EQL support should be enabled.
+ *
+ * EQL is available for all license types except {@link OperationMode#MISSING}
+ */
+ public synchronized boolean isEqlAllowed() {
+ return status.active;
+ }
+
/**
* Determine if SQL support should be enabled.
*
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java
index 7bc7ea9c42d..d948d1c7199 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java
@@ -44,6 +44,7 @@ import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata;
import org.elasticsearch.xpack.core.ccr.CCRFeatureSet;
import org.elasticsearch.xpack.core.deprecation.DeprecationInfoAction;
import org.elasticsearch.xpack.core.enrich.EnrichFeatureSet;
+import org.elasticsearch.xpack.core.eql.EqlFeatureSetUsage;
import org.elasticsearch.xpack.core.enrich.action.DeleteEnrichPolicyAction;
import org.elasticsearch.xpack.core.enrich.action.ExecuteEnrichPolicyAction;
import org.elasticsearch.xpack.core.enrich.action.GetEnrichPolicyAction;
@@ -65,9 +66,9 @@ import org.elasticsearch.xpack.core.ilm.ReadOnlyAction;
import org.elasticsearch.xpack.core.ilm.RolloverAction;
import org.elasticsearch.xpack.core.ilm.SetPriorityAction;
import org.elasticsearch.xpack.core.ilm.ShrinkAction;
-import org.elasticsearch.xpack.core.ilm.WaitForSnapshotAction;
import org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType;
import org.elasticsearch.xpack.core.ilm.UnfollowAction;
+import org.elasticsearch.xpack.core.ilm.WaitForSnapshotAction;
import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction;
import org.elasticsearch.xpack.core.ilm.action.ExplainLifecycleAction;
import org.elasticsearch.xpack.core.ilm.action.GetLifecycleAction;
@@ -545,6 +546,8 @@ public class XPackClientPlugin extends Plugin implements ActionPlugin, NetworkPl
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AnyExpression.NAME, AnyExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, FieldExpression.NAME, FieldExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, ExceptExpression.NAME, ExceptExpression::new),
+ // eql
+ new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.EQL, EqlFeatureSetUsage::new),
// sql
new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.SQL, SqlFeatureSetUsage::new),
// watcher
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java
index 931a55db350..4c9237d7f71 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackField.java
@@ -27,6 +27,8 @@ public final class XPackField {
public static final String UPGRADE = "upgrade";
// inside of YAML settings we still use xpack do not having handle issues with dashes
public static final String SETTINGS_NAME = "xpack";
+ /** Name constant for the eql feature. */
+ public static final String EQL = "eql";
/** Name constant for the sql feature. */
public static final String SQL = "sql";
/** Name constant for the rollup feature. */
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/eql/EqlFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/eql/EqlFeatureSetUsage.java
new file mode 100644
index 00000000000..55b0ff3d051
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/eql/EqlFeatureSetUsage.java
@@ -0,0 +1,51 @@
+/*
+ * 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.eql;
+
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.xpack.core.XPackFeatureSet;
+import org.elasticsearch.xpack.core.XPackField;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class EqlFeatureSetUsage extends XPackFeatureSet.Usage {
+
+ private final Map stats;
+
+ public EqlFeatureSetUsage(StreamInput in) throws IOException {
+ super(in);
+ stats = in.readMap();
+ }
+
+ public EqlFeatureSetUsage(boolean available, boolean enabled, Map stats) {
+ super(XPackField.EQL, available, enabled);
+ this.stats = stats;
+ }
+
+ public Map stats() {
+ return stats;
+ }
+
+ @Override
+ protected void innerXContent(XContentBuilder builder, Params params) throws IOException {
+ super.innerXContent(builder, params);
+ if (enabled) {
+ for (Map.Entry entry : stats.entrySet()) {
+ builder.field(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeMap(stats);
+ }
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/EqlFeatureSet.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/EqlFeatureSet.java
new file mode 100644
index 00000000000..d01cbac5a74
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/EqlFeatureSet.java
@@ -0,0 +1,82 @@
+/*
+ * 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.eql;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.xpack.core.XPackFeatureSet;
+import org.elasticsearch.xpack.core.XPackField;
+import org.elasticsearch.xpack.core.eql.EqlFeatureSetUsage;
+import org.elasticsearch.xpack.core.watcher.common.stats.Counters;
+import org.elasticsearch.xpack.eql.plugin.EqlPlugin;
+import org.elasticsearch.xpack.eql.plugin.EqlStatsAction;
+import org.elasticsearch.xpack.eql.plugin.EqlStatsRequest;
+import org.elasticsearch.xpack.eql.plugin.EqlStatsResponse;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class EqlFeatureSet implements XPackFeatureSet {
+
+ private final boolean enabled;
+ private final XPackLicenseState licenseState;
+ private final Client client;
+
+ @Inject
+ public EqlFeatureSet(Settings settings, @Nullable XPackLicenseState licenseState, Client client) {
+ this.enabled = EqlPlugin.isEnabled(settings);
+ this.licenseState = licenseState;
+ this.client = client;
+ }
+
+ @Override
+ public String name() {
+ return XPackField.EQL;
+ }
+
+ @Override
+ public boolean available() {
+ return licenseState.isEqlAllowed();
+ }
+
+ @Override
+ public boolean enabled() {
+ return enabled;
+ }
+
+ @Override
+ public Map nativeCodeInfo() {
+ return null;
+ }
+
+ @Override
+ public void usage(ActionListener listener) {
+ if (enabled) {
+ EqlStatsRequest request = new EqlStatsRequest();
+ request.includeStats(true);
+ client.execute(EqlStatsAction.INSTANCE, request, ActionListener.wrap(r -> {
+ List countersPerNode = r.getNodes()
+ .stream()
+ .map(EqlStatsResponse.NodeStatsResponse::getStats)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ Counters mergedCounters = Counters.merge(countersPerNode);
+ listener.onResponse(new EqlFeatureSetUsage(available(), enabled(), mergedCounters.toNestedMap()));
+ }, listener::onFailure));
+ } else {
+ listener.onResponse(new EqlFeatureSetUsage(available(), enabled(), Collections.emptyMap()));
+ }
+ }
+
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java
index 14cbae5e1a3..375789761d7 100644
--- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlPlugin.java
@@ -12,6 +12,7 @@ import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.IndexScopedSettings;
@@ -28,11 +29,14 @@ import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
+import org.elasticsearch.xpack.core.XPackPlugin;
+import org.elasticsearch.xpack.eql.EqlFeatureSet;
import org.elasticsearch.xpack.eql.action.EqlSearchAction;
import org.elasticsearch.xpack.eql.execution.PlanExecutor;
import org.elasticsearch.xpack.ql.index.IndexResolver;
import org.elasticsearch.xpack.ql.type.DefaultDataTypeRegistry;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -61,11 +65,18 @@ public class EqlPlugin extends Plugin implements ActionPlugin {
return Arrays.asList(planExecutor);
}
+ @Override
+ public Collection createGuiceModules() {
+ List modules = new ArrayList<>();
+ modules.add(b -> XPackPlugin.bindFeatureSet(b, EqlFeatureSet.class));
+ return modules;
+ }
@Override
public List> getActions() {
return Arrays.asList(
- new ActionHandler<>(EqlSearchAction.INSTANCE, TransportEqlSearchAction.class)
+ new ActionHandler<>(EqlSearchAction.INSTANCE, TransportEqlSearchAction.class),
+ new ActionHandler<>(EqlStatsAction.INSTANCE, TransportEqlStatsAction.class)
);
}
@@ -88,7 +99,7 @@ public class EqlPlugin extends Plugin implements ActionPlugin {
}
// TODO: this needs to be used by all plugin methods - including getActions and createComponents
- private boolean isEnabled(Settings settings) {
+ public static boolean isEnabled(Settings settings) {
return EQL_ENABLED_SETTING.get(settings);
}
@@ -104,6 +115,6 @@ public class EqlPlugin extends Plugin implements ActionPlugin {
if (isEnabled(settings) == false) {
return Collections.emptyList();
}
- return Collections.singletonList(new RestEqlSearchAction());
+ return Arrays.asList(new RestEqlSearchAction(), new RestEqlStatsAction());
}
}
\ No newline at end of file
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsAction.java
new file mode 100644
index 00000000000..ebd481b51c0
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsAction.java
@@ -0,0 +1,19 @@
+/*
+ * 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.eql.plugin;
+
+import org.elasticsearch.action.ActionType;
+
+public class EqlStatsAction extends ActionType {
+
+ public static final EqlStatsAction INSTANCE = new EqlStatsAction();
+ public static final String NAME = "cluster:monitor/xpack/eql/stats/dist";
+
+ private EqlStatsAction() {
+ super(NAME, EqlStatsResponse::new);
+ }
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsRequest.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsRequest.java
new file mode 100644
index 00000000000..6577c8ac0f4
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsRequest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.eql.plugin;
+
+import org.elasticsearch.action.support.nodes.BaseNodeRequest;
+import org.elasticsearch.action.support.nodes.BaseNodesRequest;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+
+/**
+ * Request to gather usage statistics
+ */
+public class EqlStatsRequest extends BaseNodesRequest {
+
+ private boolean includeStats;
+
+ public EqlStatsRequest() {
+ super((String[]) null);
+ }
+
+ public EqlStatsRequest(StreamInput in) throws IOException {
+ super(in);
+ includeStats = in.readBoolean();
+ }
+
+ public boolean includeStats() {
+ return includeStats;
+ }
+
+ public void includeStats(boolean includeStats) {
+ this.includeStats = includeStats;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeBoolean(includeStats);
+ }
+
+ @Override
+ public String toString() {
+ return "eql_stats";
+ }
+
+ static class NodeStatsRequest extends BaseNodeRequest {
+ boolean includeStats;
+
+ NodeStatsRequest(StreamInput in) throws IOException {
+ super(in);
+ includeStats = in.readBoolean();
+ }
+
+ NodeStatsRequest(EqlStatsRequest request) {
+ includeStats = request.includeStats();
+ }
+
+ public boolean includeStats() {
+ return includeStats;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeBoolean(includeStats);
+ }
+ }
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsResponse.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsResponse.java
new file mode 100644
index 00000000000..7e02611a8fd
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/EqlStatsResponse.java
@@ -0,0 +1,101 @@
+/*
+ * 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.eql.plugin;
+
+import org.elasticsearch.action.FailedNodeException;
+import org.elasticsearch.action.support.nodes.BaseNodeResponse;
+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.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.xpack.core.watcher.common.stats.Counters;
+
+import java.io.IOException;
+import java.util.List;
+
+public class EqlStatsResponse extends BaseNodesResponse implements ToXContentObject {
+
+ public EqlStatsResponse(StreamInput in) throws IOException {
+ super(in);
+ }
+
+ public EqlStatsResponse(ClusterName clusterName, List nodes, List failures) {
+ super(clusterName, nodes, failures);
+ }
+
+ @Override
+ protected List readNodesFrom(StreamInput in) throws IOException {
+ return in.readList(NodeStatsResponse::readNodeResponse);
+ }
+
+ @Override
+ protected void writeNodesTo(StreamOutput out, List nodes) throws IOException {
+ out.writeList(nodes);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startArray("stats");
+ for (NodeStatsResponse node : getNodes()) {
+ node.toXContent(builder, params);
+ }
+ builder.endArray();
+
+ return builder;
+ }
+
+ public static class NodeStatsResponse extends BaseNodeResponse implements ToXContentObject {
+
+ private Counters stats;
+
+ public NodeStatsResponse(StreamInput in) throws IOException {
+ super(in);
+ if (in.readBoolean()) {
+ stats = new Counters(in);
+ }
+ }
+
+ public NodeStatsResponse(DiscoveryNode node) {
+ super(node);
+ }
+
+ public Counters getStats() {
+ return stats;
+ }
+
+ public void setStats(Counters stats) {
+ this.stats = stats;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeBoolean(stats != null);
+ if (stats != null) {
+ stats.writeTo(out);
+ }
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ if (stats != null && stats.hasCounters()) {
+ builder.field("stats", stats.toNestedMap());
+ }
+ builder.endObject();
+ return builder;
+ }
+
+ static EqlStatsResponse.NodeStatsResponse readNodeResponse(StreamInput in) throws IOException {
+ return new EqlStatsResponse.NodeStatsResponse(in);
+ }
+
+ }
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlStatsAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlStatsAction.java
new file mode 100644
index 00000000000..52b241874e0
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlStatsAction.java
@@ -0,0 +1,37 @@
+/*
+ * 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.eql.plugin;
+
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.rest.BaseRestHandler;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.RestActions;
+
+import java.util.List;
+
+import static java.util.Collections.singletonList;
+import static org.elasticsearch.rest.RestRequest.Method.GET;
+
+public class RestEqlStatsAction extends BaseRestHandler {
+
+ @Override
+ public List routes() {
+ return singletonList(new Route(GET, "/_eql/stats"));
+ }
+
+ @Override
+ public String getName() {
+ return "eql_stats";
+ }
+
+ @Override
+ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {
+ EqlStatsRequest request = new EqlStatsRequest();
+ return channel -> client.execute(EqlStatsAction.INSTANCE, request, new RestActions.NodesResponseRestListener<>(channel));
+ }
+
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlStatsAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlStatsAction.java
new file mode 100644
index 00000000000..52ca7e90008
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlStatsAction.java
@@ -0,0 +1,61 @@
+/*
+ * 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.eql.plugin;
+
+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.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Performs the stats operation.
+ */
+public class TransportEqlStatsAction extends TransportNodesAction {
+
+ // the plan executor holds the metrics
+ //private final PlanExecutor planExecutor;
+
+ @Inject
+ public TransportEqlStatsAction(TransportService transportService, ClusterService clusterService,
+ ThreadPool threadPool, ActionFilters actionFilters/* , PlanExecutor planExecutor */) {
+ super(EqlStatsAction.NAME, threadPool, clusterService, transportService, actionFilters,
+ EqlStatsRequest::new, EqlStatsRequest.NodeStatsRequest::new, ThreadPool.Names.MANAGEMENT,
+ EqlStatsResponse.NodeStatsResponse.class);
+ //this.planExecutor = planExecutor;
+ }
+
+ @Override
+ protected EqlStatsResponse newResponse(EqlStatsRequest request, List nodes,
+ List failures) {
+ return new EqlStatsResponse(clusterService.getClusterName(), nodes, failures);
+ }
+
+ @Override
+ protected EqlStatsRequest.NodeStatsRequest newNodeRequest(EqlStatsRequest request) {
+ return new EqlStatsRequest.NodeStatsRequest(request);
+ }
+
+ @Override
+ protected EqlStatsResponse.NodeStatsResponse newNodeResponse(StreamInput in) throws IOException {
+ return new EqlStatsResponse.NodeStatsResponse(in);
+ }
+
+ @Override
+ protected EqlStatsResponse.NodeStatsResponse nodeOperation(EqlStatsRequest.NodeStatsRequest request) {
+ EqlStatsResponse.NodeStatsResponse statsResponse = new EqlStatsResponse.NodeStatsResponse(clusterService.localNode());
+ //statsResponse.setStats(planExecutor.metrics().stats());
+
+ return statsResponse;
+ }
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/FeatureMetric.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/FeatureMetric.java
new file mode 100644
index 00000000000..c9734214314
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/FeatureMetric.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.eql.stats;
+
+import java.util.Locale;
+
+public enum FeatureMetric {
+ SEQUENCE,
+ JOIN,
+ PIPE;
+
+ @Override
+ public String toString() {
+ return this.name().toLowerCase(Locale.ROOT);
+ }
+}
diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/Metrics.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/Metrics.java
new file mode 100644
index 00000000000..1dd9f2117a8
--- /dev/null
+++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/stats/Metrics.java
@@ -0,0 +1,85 @@
+/*
+ * 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.eql.stats;
+
+import org.elasticsearch.common.metrics.CounterMetric;
+import org.elasticsearch.xpack.core.watcher.common.stats.Counters;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Class encapsulating the metrics collected for EQL
+ */
+public class Metrics {
+ private enum OperationType {
+ FAILED, TOTAL;
+
+ @Override
+ public String toString() {
+ return this.name().toLowerCase(Locale.ROOT);
+ }
+ }
+
+ // map that holds total/failed counters for each eql "feature" (join, pipe, sequence...)
+ private final Map> featuresMetrics;
+ protected static String FPREFIX = "features.";
+
+ public Metrics() {
+ Map> fMap = new LinkedHashMap<>();
+ for (FeatureMetric metric : FeatureMetric.values()) {
+ Map metricsMap = new LinkedHashMap<>(OperationType.values().length);
+ for (OperationType type : OperationType.values()) {
+ metricsMap.put(type, new CounterMetric());
+ }
+
+ fMap.put(metric, Collections.unmodifiableMap(metricsMap));
+ }
+ featuresMetrics = Collections.unmodifiableMap(fMap);
+ }
+
+ /**
+ * Increments the "total" counter for a metric
+ * This method should be called only once per query.
+ */
+ public void total(FeatureMetric metric) {
+ inc(metric, OperationType.TOTAL);
+ }
+
+ /**
+ * Increments the "failed" counter for a metric
+ */
+ public void failed(FeatureMetric metric) {
+ inc(metric, OperationType.FAILED);
+ }
+
+ private void inc(FeatureMetric metric, OperationType op) {
+ this.featuresMetrics.get(metric).get(op).inc();
+ }
+
+ public Counters stats() {
+ Counters counters = new Counters();
+
+ // queries metrics
+ for (Entry> entry : featuresMetrics.entrySet()) {
+ String metricName = entry.getKey().toString();
+
+ for (OperationType type : OperationType.values()) {
+ long metricCounter = entry.getValue().get(type).count();
+ String operationTypeName = type.toString();
+
+ counters.inc(FPREFIX + metricName + "." + operationTypeName, metricCounter);
+ counters.inc(FPREFIX + "_all." + operationTypeName, metricCounter);
+ }
+ }
+
+ return counters;
+ }
+}
diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/EqlFeatureSetTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/EqlFeatureSetTests.java
new file mode 100644
index 00000000000..1e4f04e6930
--- /dev/null
+++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/EqlFeatureSetTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.eql;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.PlainActionFuture;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.common.xcontent.ObjectPath;
+import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.xpack.core.eql.EqlFeatureSetUsage;
+import org.elasticsearch.xpack.core.watcher.common.stats.Counters;
+import org.elasticsearch.xpack.eql.plugin.EqlStatsAction;
+import org.elasticsearch.xpack.eql.plugin.EqlStatsResponse;
+import org.junit.Before;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+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 EqlFeatureSetTests extends ESTestCase {
+
+ private XPackLicenseState licenseState;
+ private Client client;
+
+ @Before
+ public void init() throws Exception {
+ licenseState = mock(XPackLicenseState.class);
+ client = mock(Client.class);
+ ThreadPool threadPool = mock(ThreadPool.class);
+ ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
+ when(threadPool.getThreadContext()).thenReturn(threadContext);
+ when(client.threadPool()).thenReturn(threadPool);
+ }
+
+ public void testAvailable() {
+ EqlFeatureSet featureSet = new EqlFeatureSet(Settings.EMPTY, licenseState, client);
+ boolean available = randomBoolean();
+ when(licenseState.isEqlAllowed()).thenReturn(available);
+ assertThat(featureSet.available(), is(available));
+ }
+
+ public void testEnabled() {
+ boolean enabled = randomBoolean();
+ Settings.Builder settings = Settings.builder();
+ settings.put("xpack.eql.enabled", enabled);
+
+ EqlFeatureSet featureSet = new EqlFeatureSet(settings.build(), licenseState, client);
+ assertThat(featureSet.enabled(), is(enabled));
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testUsageStats() throws Exception {
+ doAnswer(mock -> {
+ ActionListener listener =
+ (ActionListener) mock.getArguments()[2];
+
+ List nodes = new ArrayList<>();
+ DiscoveryNode first = new DiscoveryNode("first", buildNewFakeTransportAddress(), Version.CURRENT);
+ EqlStatsResponse.NodeStatsResponse firstNode = new EqlStatsResponse.NodeStatsResponse(first);
+ Counters firstCounters = new Counters();
+ firstCounters.inc("foo.foo", 1);
+ firstCounters.inc("foo.bar.baz", 1);
+ firstNode.setStats(firstCounters);
+ nodes.add(firstNode);
+
+ DiscoveryNode second = new DiscoveryNode("second", buildNewFakeTransportAddress(), Version.CURRENT);
+ EqlStatsResponse.NodeStatsResponse secondNode = new EqlStatsResponse.NodeStatsResponse(second);
+ Counters secondCounters = new Counters();
+ secondCounters.inc("spam", 1);
+ secondCounters.inc("foo.bar.baz", 4);
+ secondNode.setStats(secondCounters);
+ nodes.add(secondNode);
+
+ listener.onResponse(new EqlStatsResponse(new ClusterName("whatever"), nodes, Collections.emptyList()));
+ return null;
+ }).when(client).execute(eq(EqlStatsAction.INSTANCE), any(), any());
+
+ PlainActionFuture future = new PlainActionFuture<>();
+ new EqlFeatureSet(Settings.builder().put("xpack.eql.enabled", true).build(), licenseState, client).usage(future);
+ EqlFeatureSetUsage eqlUsage = (EqlFeatureSetUsage) future.get();
+
+ long fooBarBaz = ObjectPath.eval("foo.bar.baz", eqlUsage.stats());
+ long fooFoo = ObjectPath.eval("foo.foo", eqlUsage.stats());
+ long spam = ObjectPath.eval("spam", eqlUsage.stats());
+
+ assertThat(eqlUsage.stats().keySet(), containsInAnyOrder("foo", "spam"));
+ assertThat(fooBarBaz, is(5L));
+ assertThat(fooFoo, is(1L));
+ assertThat(spam, is(1L));
+ }
+}