From 33c0dd9d3a46a417bf043ffa84cc4333b9af2e03 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 15 Aug 2016 15:24:02 +0100 Subject: [PATCH 1/2] Adds nodes usage API to monitor usages of actions The nodes usage API has 2 main endpoints `/_nodes/usage` and `/_nodes/{nodeIds}/usage` return the usage statistics for all nodes and the specified node(s) respectively. `/_nodes/usage/_clear` and `_nodes/{nodeIds}/usage/_clear clear the usage statistics for all node and the specified node(s) respectively. At the moment only one type of usage statistics is available, the rest actions usage. This records the number of times each rest action class is called and when the nodes usage api is called will return a map of rest action class name to long representing the number of times each of the action classes has been called. In following PRs I want to add usage statistics for the query types and aggregation types and this PR leaves open the ability to add other usage statistics and filter which stats are returned. Still to do: * Documentation --- .../elasticsearch/action/ActionModule.java | 15 +- .../node/usage/ClearNodeUsageResponse.java | 43 ++++ .../node/usage/ClearNodesUsageAction.java | 45 ++++ .../node/usage/ClearNodesUsageRequest.java | 37 ++++ .../usage/ClearNodesUsageRequestBuilder.java | 34 +++ .../node/usage/ClearNodesUsageResponse.java | 58 +++++ .../admin/cluster/node/usage/NodeUsage.java | 115 ++++++++++ .../cluster/node/usage/NodesUsageAction.java | 44 ++++ .../cluster/node/usage/NodesUsageRequest.java | 86 ++++++++ .../node/usage/NodesUsageRequestBuilder.java | 34 +++ .../node/usage/NodesUsageResponse.java | 85 ++++++++ .../usage/TransportClearNodesUsageAction.java | 109 +++++++++ .../node/usage/TransportNodesUsageAction.java | 105 +++++++++ .../client/ClusterAdminClient.java | 62 +++++- .../org/elasticsearch/client/Requests.java | 28 +++ .../client/support/AbstractClient.java | 38 ++++ .../client/transport/TransportClient.java | 3 +- .../java/org/elasticsearch/node/Node.java | 5 +- .../elasticsearch/rest/RestController.java | 9 +- .../cluster/RestClearNodesUsageAction.java | 76 +++++++ .../admin/cluster/RestNodesUsageAction.java | 89 ++++++++ .../org/elasticsearch/usage/UsageService.java | 91 ++++++++ .../elasticsearch/http/HttpServerTests.java | 7 +- .../rest/RestControllerTests.java | 12 +- .../rest/RestFilterChainTests.java | 12 +- .../action/cat/RestIndicesActionTests.java | 7 +- .../action/cat/RestRecoveryActionTests.java | 7 +- .../usage/UsageServiceTests.java | 100 +++++++++ .../test/rest/NodeRestUsageIT.java | 206 ++++++++++++++++++ .../rest-api-spec/api/nodes.usage.json | 38 ++++ .../test/rest/ESRestTestCase.java | 7 + 31 files changed, 1592 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodeUsageResponse.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageAction.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequest.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequestBuilder.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageResponse.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodeUsage.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageAction.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequest.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequestBuilder.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageResponse.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportClearNodesUsageAction.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportNodesUsageAction.java create mode 100644 core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearNodesUsageAction.java create mode 100644 core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesUsageAction.java create mode 100644 core/src/main/java/org/elasticsearch/usage/UsageService.java create mode 100644 core/src/test/java/org/elasticsearch/usage/UsageServiceTests.java create mode 100644 distribution/integ-test-zip/src/test/java/org/elasticsearch/test/rest/NodeRestUsageIT.java create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/nodes.usage.json diff --git a/core/src/main/java/org/elasticsearch/action/ActionModule.java b/core/src/main/java/org/elasticsearch/action/ActionModule.java index 1be1ddda9a470..d3e6fdb79e461 100644 --- a/core/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/core/src/main/java/org/elasticsearch/action/ActionModule.java @@ -43,6 +43,10 @@ import org.elasticsearch.action.admin.cluster.node.tasks.get.TransportGetTaskAction; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksAction; import org.elasticsearch.action.admin.cluster.node.tasks.list.TransportListTasksAction; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageAction; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageAction; +import org.elasticsearch.action.admin.cluster.node.usage.TransportClearNodesUsageAction; +import org.elasticsearch.action.admin.cluster.node.usage.TransportNodesUsageAction; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryAction; import org.elasticsearch.action.admin.cluster.repositories.delete.TransportDeleteRepositoryAction; import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesAction; @@ -211,6 +215,7 @@ import org.elasticsearch.rest.action.RestFieldStatsAction; import org.elasticsearch.rest.action.RestMainAction; import org.elasticsearch.rest.action.admin.cluster.RestCancelTasksAction; +import org.elasticsearch.rest.action.admin.cluster.RestClearNodesUsageAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterAllocationExplainAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterGetSettingsAction; import org.elasticsearch.rest.action.admin.cluster.RestClusterHealthAction; @@ -231,6 +236,7 @@ import org.elasticsearch.rest.action.admin.cluster.RestNodesHotThreadsAction; import org.elasticsearch.rest.action.admin.cluster.RestNodesInfoAction; import org.elasticsearch.rest.action.admin.cluster.RestNodesStatsAction; +import org.elasticsearch.rest.action.admin.cluster.RestNodesUsageAction; import org.elasticsearch.rest.action.admin.cluster.RestPendingClusterTasksAction; import org.elasticsearch.rest.action.admin.cluster.RestPutRepositoryAction; import org.elasticsearch.rest.action.admin.cluster.RestPutStoredScriptAction; @@ -309,6 +315,7 @@ import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.rest.action.search.RestSearchScrollAction; import org.elasticsearch.rest.action.search.RestSuggestAction; +import org.elasticsearch.usage.UsageService; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; @@ -328,7 +335,7 @@ public class ActionModule extends AbstractModule { private final RestController restController; public ActionModule(boolean ingestEnabled, boolean transportClient, Settings settings, IndexNameExpressionResolver resolver, - ClusterSettings clusterSettings, List actionPlugins) { + ClusterSettings clusterSettings, List actionPlugins, UsageService usageService) { this.transportClient = transportClient; this.settings = settings; this.actionPlugins = actionPlugins; @@ -337,7 +344,7 @@ public ActionModule(boolean ingestEnabled, boolean transportClient, Settings set autoCreateIndex = transportClient ? null : new AutoCreateIndex(settings, clusterSettings, resolver); destructiveOperations = new DestructiveOperations(settings, clusterSettings); Set headers = actionPlugins.stream().flatMap(p -> p.getRestHeaders().stream()).collect(Collectors.toSet()); - restController = new RestController(settings, headers); + restController = new RestController(settings, headers, usageService); } public Map> getActions() { @@ -366,6 +373,8 @@ public , Response extends ActionResponse> actions.register(MainAction.INSTANCE, TransportMainAction.class); actions.register(NodesInfoAction.INSTANCE, TransportNodesInfoAction.class); actions.register(NodesStatsAction.INSTANCE, TransportNodesStatsAction.class); + actions.register(NodesUsageAction.INSTANCE, TransportNodesUsageAction.class); + actions.register(ClearNodesUsageAction.INSTANCE, TransportClearNodesUsageAction.class); actions.register(NodesHotThreadsAction.INSTANCE, TransportNodesHotThreadsAction.class); actions.register(ListTasksAction.INSTANCE, TransportListTasksAction.class); actions.register(GetTaskAction.INSTANCE, TransportGetTaskAction.class); @@ -480,6 +489,8 @@ static Set> setupRestHandlers(List ac registerRestHandler(handlers, RestMainAction.class); registerRestHandler(handlers, RestNodesInfoAction.class); registerRestHandler(handlers, RestNodesStatsAction.class); + registerRestHandler(handlers, RestNodesUsageAction.class); + registerRestHandler(handlers, RestClearNodesUsageAction.class); registerRestHandler(handlers, RestNodesHotThreadsAction.class); registerRestHandler(handlers, RestClusterAllocationExplainAction.class); registerRestHandler(handlers, RestClusterStatsAction.class); diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodeUsageResponse.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodeUsageResponse.java new file mode 100644 index 0000000000000..732ca8b31c68e --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodeUsageResponse.java @@ -0,0 +1,43 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +public class ClearNodeUsageResponse extends BaseNodeResponse implements ToXContent { + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("acknowledged", true); + return builder; + } + + public static ClearNodeUsageResponse readNodeStats(StreamInput in) throws IOException { + ClearNodeUsageResponse noderesponse = new ClearNodeUsageResponse(); + noderesponse.readFrom(in); + return noderesponse; + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageAction.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageAction.java new file mode 100644 index 0000000000000..0d790000993eb --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageAction.java @@ -0,0 +1,45 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +public class ClearNodesUsageAction extends Action { + + public static final ClearNodesUsageAction INSTANCE = new ClearNodesUsageAction(); + public static final String NAME = "cluster:monitor/nodes/usage/clear"; + + protected ClearNodesUsageAction() { + super(NAME); + } + + @Override + public ClearNodesUsageRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new ClearNodesUsageRequestBuilder(client, this); + } + + @Override + public ClearNodesUsageResponse newResponse() { + return new ClearNodesUsageResponse(); + } + + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequest.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequest.java new file mode 100644 index 0000000000000..eac0f03ff49de --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequest.java @@ -0,0 +1,37 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.support.nodes.BaseNodesRequest; + +public class ClearNodesUsageRequest extends BaseNodesRequest { + + public ClearNodesUsageRequest() { + super(); + } + + /** + * Get usage from nodes based on the nodes ids specified. If none are + * passed, usage for all nodes will be returned. + */ + public ClearNodesUsageRequest(String... nodesIds) { + super(nodesIds); + } +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequestBuilder.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequestBuilder.java new file mode 100644 index 0000000000000..20944857557a6 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageRequestBuilder.java @@ -0,0 +1,34 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.support.nodes.NodesOperationRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +public class ClearNodesUsageRequestBuilder + extends NodesOperationRequestBuilder { + + public ClearNodesUsageRequestBuilder(ElasticsearchClient client, + Action action) { + super(client, action, new ClearNodesUsageRequest()); + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageResponse.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageResponse.java new file mode 100644 index 0000000000000..10c856584a0d5 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/ClearNodesUsageResponse.java @@ -0,0 +1,58 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; + +public class ClearNodesUsageResponse extends BaseNodesResponse implements ToXContent { + + ClearNodesUsageResponse() { + super(); + } + + public ClearNodesUsageResponse(ClusterName clusterName, List nodes, List failures) { + super(clusterName, nodes, failures); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder; + } + + @Override + protected List readNodesFrom(StreamInput in) throws IOException { + return in.readList(ClearNodeUsageResponse::readNodeStats); + } + + @Override + protected void writeNodesTo(StreamOutput out, List nodes) throws IOException { + out.writeStreamableList(nodes); + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodeUsage.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodeUsage.java new file mode 100644 index 0000000000000..954e64e8caf33 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodeUsage.java @@ -0,0 +1,115 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +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.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; + +public class NodeUsage extends BaseNodeResponse implements ToXContent { + + private long timestamp; + private long sinceTime; + private Map restUsage; + + NodeUsage() { + } + + public static NodeUsage readNodeStats(StreamInput in) throws IOException { + NodeUsage nodeInfo = new NodeUsage(); + nodeInfo.readFrom(in); + return nodeInfo; + } + + /** + * @param node + * the node these statistics were collected from + * @param timestamp + * the timestamp for when these statistics were collected + * @param sinceTime + * the timestamp for when the collection of these statistics + * started + * @param restUsage + * a map containing the counts of the number of times each REST + * endpoint has been called + */ + public NodeUsage(DiscoveryNode node, long timestamp, long sinceTime, Map restUsage) { + super(node); + this.timestamp = timestamp; + this.sinceTime = sinceTime; + this.restUsage = restUsage; + } + + /** + * @return the timestamp for when these statistics were collected + */ + public long getTimestamp() { + return timestamp; + } + + /** + * @return the timestamp for when the collection of these statistics started + */ + public long getSinceTime() { + return sinceTime; + } + + /** + * @return a map containing the counts of the number of times each REST + * endpoint has been called + */ + public Map getRestUsage() { + return restUsage; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("since", sinceTime); + if (restUsage != null) { + builder.field("rest_actions"); + builder.map(restUsage); + } + return builder; + } + + @SuppressWarnings("unchecked") + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + timestamp = in.readLong(); + sinceTime = in.readLong(); + restUsage = (Map) in.readGenericValue(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeLong(timestamp); + out.writeLong(sinceTime); + out.writeGenericValue(restUsage); + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageAction.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageAction.java new file mode 100644 index 0000000000000..6a9bebaa06bc5 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageAction.java @@ -0,0 +1,44 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +public class NodesUsageAction extends Action { + + public static final NodesUsageAction INSTANCE = new NodesUsageAction(); + public static final String NAME = "cluster:monitor/nodes/usage"; + + protected NodesUsageAction() { + super(NAME); + } + + @Override + public NodesUsageRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new NodesUsageRequestBuilder(client, this); + } + + @Override + public NodesUsageResponse newResponse() { + return new NodesUsageResponse(); + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequest.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequest.java new file mode 100644 index 0000000000000..dcd60e61d8b90 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequest.java @@ -0,0 +1,86 @@ +/* + * 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.action.admin.cluster.node.usage; + +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; + +public class NodesUsageRequest extends BaseNodesRequest { + + private boolean restActions; + + public NodesUsageRequest() { + super(); + } + + /** + * Get usage from nodes based on the nodes ids specified. If none are + * passed, usage for all nodes will be returned. + */ + public NodesUsageRequest(String... nodesIds) { + super(nodesIds); + } + + /** + * Sets all the request flags. + */ + public NodesUsageRequest all() { + this.restActions = true; + return this; + } + + /** + * Clears all the request flags. + */ + public NodesUsageRequest clear() { + this.restActions = false; + return this; + } + + /** + * Should the node rest actions usage statistics be returned. + */ + public boolean restActions() { + return this.restActions; + } + + /** + * Should the node rest actions usage statistics be returned. + */ + public NodesUsageRequest restActions(boolean restActions) { + this.restActions = restActions; + return this; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.restActions = in.readBoolean(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeBoolean(restActions); + } +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequestBuilder.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequestBuilder.java new file mode 100644 index 0000000000000..76d14556b9c4a --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageRequestBuilder.java @@ -0,0 +1,34 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.support.nodes.NodesOperationRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +public class NodesUsageRequestBuilder + extends NodesOperationRequestBuilder { + + public NodesUsageRequestBuilder(ElasticsearchClient client, + Action action) { + super(client, action, new NodesUsageRequest()); + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageResponse.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageResponse.java new file mode 100644 index 0000000000000..e60e7a05cf821 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/NodesUsageResponse.java @@ -0,0 +1,85 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; + +import java.io.IOException; +import java.util.List; + +/** + * The response for the nodes usage api which contains the individual usage + * statistics for all nodes queried. + */ +public class NodesUsageResponse extends BaseNodesResponse implements ToXContent { + + NodesUsageResponse() { + } + + public NodesUsageResponse(ClusterName clusterName, List nodes, List failures) { + super(clusterName, nodes, failures); + } + + @Override + protected List readNodesFrom(StreamInput in) throws IOException { + return in.readList(NodeUsage::readNodeStats); + } + + @Override + protected void writeNodesTo(StreamOutput out, List nodes) throws IOException { + out.writeStreamableList(nodes); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject("nodes"); + for (NodeUsage nodeUsage : getNodes()) { + builder.startObject(nodeUsage.getNode().getId()); + builder.field("timestamp", nodeUsage.getTimestamp()); + nodeUsage.toXContent(builder, params); + + builder.endObject(); + } + builder.endObject(); + + return builder; + } + + @Override + public String toString() { + try { + XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + builder.startObject(); + toXContent(builder, EMPTY_PARAMS); + builder.endObject(); + return builder.string(); + } catch (IOException e) { + return "{ \"error\" : \"" + e.getMessage() + "\"}"; + } + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportClearNodesUsageAction.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportClearNodesUsageAction.java new file mode 100644 index 0000000000000..1dfac5c54ccd1 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportClearNodesUsageAction.java @@ -0,0 +1,109 @@ +/* + * 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.action.admin.cluster.node.usage; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.nodes.BaseNodeRequest; +import org.elasticsearch.action.support.nodes.TransportNodesAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.node.service.NodeService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.usage.UsageService; + +import java.io.IOException; +import java.util.List; + +public class TransportClearNodesUsageAction extends + TransportNodesAction { + + private UsageService usageService; + + @Inject + public TransportClearNodesUsageAction(Settings settings, ThreadPool threadPool, ClusterService clusterService, + TransportService transportService, NodeService nodeService, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver, UsageService usageService) { + super(settings, ClearNodesUsageAction.NAME, threadPool, clusterService, transportService, actionFilters, + indexNameExpressionResolver, ClearNodesUsageRequest::new, ClearNodeUsageRequest::new, ThreadPool.Names.MANAGEMENT, + ClearNodeUsageResponse.class); + this.usageService = usageService; + } + + @Override + protected ClearNodesUsageResponse newResponse(ClearNodesUsageRequest request, List responses, + List failures) { + return new ClearNodesUsageResponse(clusterService.getClusterName(), responses, failures); + } + + @Override + protected ClearNodeUsageRequest newNodeRequest(String nodeId, ClearNodesUsageRequest request) { + return new ClearNodeUsageRequest(nodeId, request); + } + + @Override + protected ClearNodeUsageResponse newNodeResponse() { + return new ClearNodeUsageResponse(); + } + + @Override + protected ClearNodeUsageResponse nodeOperation(ClearNodeUsageRequest nodeUsageRequest) { + usageService.clear(); + return new ClearNodeUsageResponse(); + } + + @Override + protected boolean accumulateExceptions() { + return false; + } + + public static class ClearNodeUsageRequest extends BaseNodeRequest { + + ClearNodesUsageRequest request; + + public ClearNodeUsageRequest() { + } + + ClearNodeUsageRequest(String nodeId, ClearNodesUsageRequest request) { + super(nodeId); + this.request = request; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + request = new ClearNodesUsageRequest(); + request.readFrom(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + request.writeTo(out); + } + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportNodesUsageAction.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportNodesUsageAction.java new file mode 100644 index 0000000000000..ee3c624574bb5 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/usage/TransportNodesUsageAction.java @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.node.usage; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.nodes.BaseNodeRequest; +import org.elasticsearch.action.support.nodes.TransportNodesAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.node.service.NodeService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.usage.UsageService; + +import java.io.IOException; +import java.util.List; + +public class TransportNodesUsageAction + extends TransportNodesAction { + + private UsageService usageService; + + @Inject + public TransportNodesUsageAction(Settings settings, ThreadPool threadPool, ClusterService clusterService, + TransportService transportService, NodeService nodeService, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver, UsageService usageService) { + super(settings, NodesUsageAction.NAME, threadPool, clusterService, transportService, actionFilters, indexNameExpressionResolver, + NodesUsageRequest::new, NodeUsageRequest::new, ThreadPool.Names.MANAGEMENT, NodeUsage.class); + this.usageService = usageService; + } + + @Override + protected NodesUsageResponse newResponse(NodesUsageRequest request, List responses, List failures) { + return new NodesUsageResponse(clusterService.getClusterName(), responses, failures); + } + + @Override + protected NodeUsageRequest newNodeRequest(String nodeId, NodesUsageRequest request) { + return new NodeUsageRequest(nodeId, request); + } + + @Override + protected NodeUsage newNodeResponse() { + return new NodeUsage(); + } + + @Override + protected NodeUsage nodeOperation(NodeUsageRequest nodeUsageRequest) { + NodesUsageRequest request = nodeUsageRequest.request; + return usageService.getUsageStats(request.restActions()); + } + + @Override + protected boolean accumulateExceptions() { + return false; + } + + public static class NodeUsageRequest extends BaseNodeRequest { + + NodesUsageRequest request; + + public NodeUsageRequest() { + } + + NodeUsageRequest(String nodeId, NodesUsageRequest request) { + super(nodeId); + this.request = request; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + request = new NodesUsageRequest(); + request.readFrom(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + request.writeTo(out); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/client/ClusterAdminClient.java b/core/src/main/java/org/elasticsearch/client/ClusterAdminClient.java index 9e0d1a941192c..822996f5db232 100644 --- a/core/src/main/java/org/elasticsearch/client/ClusterAdminClient.java +++ b/core/src/main/java/org/elasticsearch/client/ClusterAdminClient.java @@ -45,6 +45,12 @@ import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequestBuilder; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageRequest; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageRequestBuilder; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageResponse; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageRequest; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageRequestBuilder; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageResponse; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequestBuilder; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryResponse; @@ -264,8 +270,60 @@ public interface ClusterAdminClient extends ElasticsearchClient { NodesStatsRequestBuilder prepareNodesStats(String... nodesIds); /** - * Returns top N hot-threads samples per node. The hot-threads are only sampled - * for the node ids specified in the request. + * Nodes usage of the cluster. + * + * @param request + * The nodes usage request + * @return The result future + * @see org.elasticsearch.client.Requests#nodesUsageRequest(String...) + */ + ActionFuture nodesUsage(NodesUsageRequest request); + + /** + * Nodes usage of the cluster. + * + * @param request + * The nodes usage request + * @param listener + * A listener to be notified with a result + * @see org.elasticsearch.client.Requests#nodesUsageRequest(String...) + */ + void nodesUsage(NodesUsageRequest request, ActionListener listener); + + /** + * Nodes usage of the cluster. + */ + NodesUsageRequestBuilder prepareNodesUsage(String... nodesIds); + + /** + * Clear Nodes Usage statistics. + * + * @param request + * The clear nodes usage request + * @return The result future + * @see org.elasticsearch.client.Requests#clearNodesUsageRequest(String...) + */ + ActionFuture clearNodesUsage(ClearNodesUsageRequest request); + + /** + * Clear Nodes Usage statistics. + * + * @param request + * The clear nodes usage request + * @param listener + * A listener to be notified with a result + * @see org.elasticsearch.client.Requests#clearNodesUsageRequest(String...) + */ + void clearNodesUsage(ClearNodesUsageRequest request, ActionListener listener); + + /** + * Clear Nodes Usage statistics. + */ + ClearNodesUsageRequestBuilder prepareClearNodesUsage(String... nodesIds); + + /** + * Returns top N hot-threads samples per node. The hot-threads are only + * sampled for the node ids specified in the request. */ ActionFuture nodesHotThreads(NodesHotThreadsRequest request); diff --git a/core/src/main/java/org/elasticsearch/client/Requests.java b/core/src/main/java/org/elasticsearch/client/Requests.java index 6d652bf39d0a9..a19ea5f52da21 100644 --- a/core/src/main/java/org/elasticsearch/client/Requests.java +++ b/core/src/main/java/org/elasticsearch/client/Requests.java @@ -25,6 +25,8 @@ import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest; import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskRequest; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageRequest; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageRequest; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRequest; import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; @@ -387,6 +389,32 @@ public static NodesStatsRequest nodesStatsRequest(String... nodesIds) { return new NodesStatsRequest(nodesIds); } + /** + * Creates a nodes usage request against one or more nodes. Pass + * null or an empty array for all nodes. + * + * @param nodesIds + * The nodes ids to get the usage for + * @return The nodes usage request + * @see org.elasticsearch.client.ClusterAdminClient#nodesUsage(org.elasticsearch.action.admin.cluster.node.usage.NodesUsageRequest) + */ + public static NodesUsageRequest nodesUsageRequest(String... nodesIds) { + return new NodesUsageRequest(nodesIds); + } + + /** + * Creates a clear nodes usage request against one or more nodes. Pass + * null or an empty array for all nodes. + * + * @param nodesIds + * The nodes ids to clear the usage for + * @return The clear nodes usage request + * @see ClusterAdminClient#clearNodesUsage(org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageRequest) + */ + public static ClearNodesUsageRequest clearNodesUsageRequest(String... nodesIds) { + return new ClearNodesUsageRequest(nodesIds); + } + /** * Creates a cluster stats request. * diff --git a/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java b/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java index c3816d8d37fad..d427a12f679fd 100644 --- a/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java +++ b/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java @@ -57,6 +57,14 @@ import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequestBuilder; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageAction; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageRequest; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageRequestBuilder; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageResponse; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageAction; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageRequest; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageRequestBuilder; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageResponse; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryAction; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequestBuilder; @@ -811,6 +819,36 @@ public NodesStatsRequestBuilder prepareNodesStats(String... nodesIds) { return new NodesStatsRequestBuilder(this, NodesStatsAction.INSTANCE).setNodesIds(nodesIds); } + @Override + public ActionFuture nodesUsage(final NodesUsageRequest request) { + return execute(NodesUsageAction.INSTANCE, request); + } + + @Override + public void nodesUsage(final NodesUsageRequest request, final ActionListener listener) { + execute(NodesUsageAction.INSTANCE, request, listener); + } + + @Override + public NodesUsageRequestBuilder prepareNodesUsage(String... nodesIds) { + return new NodesUsageRequestBuilder(this, NodesUsageAction.INSTANCE).setNodesIds(nodesIds); + } + + @Override + public ActionFuture clearNodesUsage(final ClearNodesUsageRequest request) { + return execute(ClearNodesUsageAction.INSTANCE, request); + } + + @Override + public void clearNodesUsage(final ClearNodesUsageRequest request, final ActionListener listener) { + execute(ClearNodesUsageAction.INSTANCE, request, listener); + } + + @Override + public ClearNodesUsageRequestBuilder prepareClearNodesUsage(String... nodesIds) { + return new ClearNodesUsageRequestBuilder(this, ClearNodesUsageAction.INSTANCE).setNodesIds(nodesIds); + } + @Override public ActionFuture clusterStats(ClusterStatsRequest request) { return execute(ClusterStatsAction.INSTANCE, request); diff --git a/core/src/main/java/org/elasticsearch/client/transport/TransportClient.java b/core/src/main/java/org/elasticsearch/client/transport/TransportClient.java index f7ce9f929bdc0..01ce5098e8bf2 100644 --- a/core/src/main/java/org/elasticsearch/client/transport/TransportClient.java +++ b/core/src/main/java/org/elasticsearch/client/transport/TransportClient.java @@ -53,7 +53,6 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.transport.TransportService; - import java.io.Closeable; import java.util.ArrayList; import java.util.Arrays; @@ -138,7 +137,7 @@ private static ClientTemplate buildTemplate(Settings providedSettings, Settings modules.add(b -> b.bind(ThreadPool.class).toInstance(threadPool)); modules.add(searchModule); ActionModule actionModule = new ActionModule(false, true, settings, null, settingsModule.getClusterSettings(), - pluginsService.filterPlugins(ActionPlugin.class)); + pluginsService.filterPlugins(ActionPlugin.class), null); modules.add(actionModule); pluginsService.processModules(modules); diff --git a/core/src/main/java/org/elasticsearch/node/Node.java b/core/src/main/java/org/elasticsearch/node/Node.java index 402064e88d299..15f9b4fa021bb 100644 --- a/core/src/main/java/org/elasticsearch/node/Node.java +++ b/core/src/main/java/org/elasticsearch/node/Node.java @@ -120,6 +120,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.tribe.TribeService; +import org.elasticsearch.usage.UsageService; import org.elasticsearch.watcher.ResourceWatcherService; import javax.management.MBeanServerPermission; @@ -318,6 +319,7 @@ protected Node(final Environment environment, Collection resourcesToClose.add(tribeService); final IngestService ingestService = new IngestService(settings, threadPool, this.environment, scriptModule.getScriptService(), analysisModule.getAnalysisRegistry(), pluginsService.filterPlugins(IngestPlugin.class)); + final UsageService usageService = new UsageService(clusterService::localNode, settings); ModulesBuilder modules = new ModulesBuilder(); // plugin modules must be added here, before others or we can get crazy injection errors... @@ -338,7 +340,7 @@ protected Node(final Environment environment, Collection modules.add(searchModule); modules.add(new ActionModule(DiscoveryNode.isIngestNode(settings), false, settings, clusterModule.getIndexNameExpressionResolver(), settingsModule.getClusterSettings(), - pluginsService.filterPlugins(ActionPlugin.class))); + pluginsService.filterPlugins(ActionPlugin.class), usageService)); modules.add(new GatewayModule()); modules.add(new RepositoriesModule(this.environment, pluginsService.filterPlugins(RepositoryPlugin.class))); pluginsService.processModules(modules); @@ -386,6 +388,7 @@ protected Node(final Environment environment, Collection b.bind(AnalysisRegistry.class).toInstance(analysisModule.getAnalysisRegistry()); b.bind(IngestService.class).toInstance(ingestService); b.bind(NamedWriteableRegistry.class).toInstance(namedWriteableRegistry); + b.bind(UsageService.class).toInstance(usageService); b.bind(MetaDataUpgrader.class).toInstance(metaDataUpgrader); b.bind(MetaStateService.class).toInstance(metaStateService); b.bind(IndicesService.class).toInstance(indicesService); diff --git a/core/src/main/java/org/elasticsearch/rest/RestController.java b/core/src/main/java/org/elasticsearch/rest/RestController.java index e63f35884e88c..1efcd3feed85a 100644 --- a/core/src/main/java/org/elasticsearch/rest/RestController.java +++ b/core/src/main/java/org/elasticsearch/rest/RestController.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.usage.UsageService; import java.io.IOException; import java.util.Arrays; @@ -58,9 +59,12 @@ public class RestController extends AbstractLifecycleComponent { // non volatile since the assumption is that pre processors are registered on startup private RestFilter[] filters = new RestFilter[0]; - public RestController(Settings settings, Set headersToCopy) { + private UsageService usageService; + + public RestController(Settings settings, Set headersToCopy, UsageService usageService) { super(settings); this.headersToCopy = headersToCopy; + this.usageService = usageService; } @Override @@ -239,6 +243,9 @@ boolean checkRequestParameters(final RestRequest request, final RestChannel chan void executeHandler(RestRequest request, RestChannel channel, NodeClient client) throws Exception { final RestHandler handler = getHandler(request); if (handler != null) { + if (usageService != null) { + usageService.addRestCall(handler.getClass().getName()); + } handler.handleRequest(request, channel, client); } else { if (request.method() == RestRequest.Method.OPTIONS) { diff --git a/core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearNodesUsageAction.java b/core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearNodesUsageAction.java new file mode 100644 index 0000000000000..c216370e6275c --- /dev/null +++ b/core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearNodesUsageAction.java @@ -0,0 +1,76 @@ +/* + * 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.action.admin.cluster.node.usage.ClearNodesUsageRequest; +import org.elasticsearch.action.admin.cluster.node.usage.ClearNodesUsageResponse; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestActions; +import org.elasticsearch.rest.action.RestBuilderListener; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class RestClearNodesUsageAction extends BaseRestHandler { + + @Inject + public RestClearNodesUsageAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(POST, "/_nodes/usage/_clear", this); + controller.registerHandler(POST, "/_nodes/{nodeId}/usage/_clear", this); + } + + @Override + public void handleRequest(final RestRequest request, final RestChannel channel, final NodeClient client) { + String[] nodesIds = Strings.splitStringByCommaToArray(request.param("nodeId")); + + ClearNodesUsageRequest nodesUsageRequest = new ClearNodesUsageRequest(nodesIds); + nodesUsageRequest.timeout(request.param("timeout")); + + client.admin().cluster().clearNodesUsage(nodesUsageRequest, new RestBuilderListener(channel) { + + @Override + public RestResponse buildResponse(ClearNodesUsageResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + RestActions.buildNodesHeader(builder, channel.request(), response); + builder.field("cluster_name", response.getClusterName().value()); + response.toXContent(builder, channel.request()); + builder.endObject(); + + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + + @Override + public boolean canTripCircuitBreaker() { + return false; + } +} diff --git a/core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesUsageAction.java b/core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesUsageAction.java new file mode 100644 index 0000000000000..42eb529abe54f --- /dev/null +++ b/core/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesUsageAction.java @@ -0,0 +1,89 @@ +/* + * 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.action.admin.cluster.node.usage.NodesUsageRequest; +import org.elasticsearch.action.admin.cluster.node.usage.NodesUsageResponse; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestActions; +import org.elasticsearch.rest.action.RestBuilderListener; + +import java.util.Set; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class RestNodesUsageAction extends BaseRestHandler { + + @Inject + public RestNodesUsageAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(GET, "/_nodes/usage", this); + controller.registerHandler(GET, "/_nodes/{nodeId}/usage", this); + + controller.registerHandler(GET, "/_nodes/usage/{metric}", this); + controller.registerHandler(GET, "/_nodes/{nodeId}/usage/{metric}", this); + } + + @Override + public void handleRequest(final RestRequest request, final RestChannel channel, final NodeClient client) { + String[] nodesIds = Strings.splitStringByCommaToArray(request.param("nodeId")); + Set metrics = Strings.splitStringByCommaToSet(request.param("metric", "_all")); + + NodesUsageRequest nodesUsageRequest = new NodesUsageRequest(nodesIds); + nodesUsageRequest.timeout(request.param("timeout")); + + if (metrics.size() == 1 && metrics.contains("_all")) { + nodesUsageRequest.all(); + } else { + nodesUsageRequest.clear(); + nodesUsageRequest.restActions(metrics.contains("rest_actions")); + } + + client.admin().cluster().nodesUsage(nodesUsageRequest, new RestBuilderListener(channel) { + + @Override + public RestResponse buildResponse(NodesUsageResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + RestActions.buildNodesHeader(builder, channel.request(), response); + builder.field("cluster_name", response.getClusterName().value()); + response.toXContent(builder, channel.request()); + builder.endObject(); + + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + + @Override + public boolean canTripCircuitBreaker() { + return false; + } +} diff --git a/core/src/main/java/org/elasticsearch/usage/UsageService.java b/core/src/main/java/org/elasticsearch/usage/UsageService.java new file mode 100644 index 0000000000000..514dca8ad9352 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/usage/UsageService.java @@ -0,0 +1,91 @@ +/* + * 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.usage; + +import org.elasticsearch.action.admin.cluster.node.usage.NodeUsage; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestHandler; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +/** + * A service to monitor usage of Elasticsearch features. + */ +public class UsageService extends AbstractComponent { + + private final Supplier localNodeSupplier; + private final Map restUsage; + private long sinceTime; + + @Inject + public UsageService(Supplier localNodeSupplier, Settings settings) { + super(settings); + this.localNodeSupplier = localNodeSupplier; + this.restUsage = new ConcurrentHashMap<>(); + this.sinceTime = System.currentTimeMillis(); + } + + /** + * record a call to a REST endpoint. + * + * @param actionName + * the class name of the {@link RestHandler} called for this + * endpoint. + */ + public void addRestCall(String actionName) { + AtomicLong counter = restUsage.computeIfAbsent(actionName, key -> new AtomicLong()); + counter.getAndIncrement(); + } + + public void clear() { + this.sinceTime = System.currentTimeMillis(); + this.restUsage.clear(); + } + + /** + * Get the current usage statistics for this node. + * + * @param restActions + * whether to include rest action usage in the returned + * statistics + * @return the {@link NodeUsage} representing the usage statistics for this + * node + */ + public NodeUsage getUsageStats(boolean restActions) { + Map restUsageMap; + if (restActions) { + restUsageMap = new HashMap<>(); + restUsage.forEach((key, value) -> { + restUsageMap.put(key, value.get()); + }); + } else { + restUsageMap = null; + } + return new NodeUsage(localNodeSupplier.get(), System.currentTimeMillis(), sinceTime, restUsageMap); + } + +} diff --git a/core/src/test/java/org/elasticsearch/http/HttpServerTests.java b/core/src/test/java/org/elasticsearch/http/HttpServerTests.java index 87167cdb73345..569203843ec73 100644 --- a/core/src/test/java/org/elasticsearch/http/HttpServerTests.java +++ b/core/src/test/java/org/elasticsearch/http/HttpServerTests.java @@ -21,6 +21,8 @@ import java.util.Collections; import java.util.Map; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -41,6 +43,7 @@ import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.usage.UsageService; import org.junit.Before; public class HttpServerTests extends ESTestCase { @@ -60,7 +63,9 @@ public void setup() { inFlightRequestsBreaker = circuitBreakerService.getBreaker(CircuitBreaker.IN_FLIGHT_REQUESTS); HttpServerTransport httpServerTransport = new TestHttpServerTransport(); - RestController restController = new RestController(settings, Collections.emptySet()); + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, settings); + RestController restController = new RestController(settings, Collections.emptySet(), usageService); restController.registerHandler(RestRequest.Method.GET, "/", (request, channel, client) -> channel.sendResponse( new BytesRestResponse(RestStatus.OK, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY))); diff --git a/core/src/test/java/org/elasticsearch/rest/RestControllerTests.java b/core/src/test/java/org/elasticsearch/rest/RestControllerTests.java index dca0a16f0f085..fb11804f4ebf6 100644 --- a/core/src/test/java/org/elasticsearch/rest/RestControllerTests.java +++ b/core/src/test/java/org/elasticsearch/rest/RestControllerTests.java @@ -26,12 +26,16 @@ import java.util.Map; import java.util.Set; +import org.elasticsearch.Version; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.LocalTransportAddress; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.usage.UsageService; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -44,7 +48,9 @@ public class RestControllerTests extends ESTestCase { public void testApplyRelevantHeaders() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); Set headers = new HashSet<>(Arrays.asList("header.1", "header.2")); - final RestController restController = new RestController(Settings.EMPTY, headers) { + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, Settings.EMPTY); + final RestController restController = new RestController(Settings.EMPTY, headers, usageService) { @Override boolean checkRequestParameters(RestRequest request, RestChannel channel) { return true; @@ -69,7 +75,9 @@ void executeHandler(RestRequest request, RestChannel channel, NodeClient client) } public void testCanTripCircuitBreaker() throws Exception { - RestController controller = new RestController(Settings.EMPTY, Collections.emptySet()); + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, Settings.EMPTY); + RestController controller = new RestController(Settings.EMPTY, Collections.emptySet(), usageService); // trip circuit breaker by default controller.registerHandler(RestRequest.Method.GET, "/trip", new FakeRestHandler(true)); controller.registerHandler(RestRequest.Method.GET, "/do-not-trip", new FakeRestHandler(false)); diff --git a/core/src/test/java/org/elasticsearch/rest/RestFilterChainTests.java b/core/src/test/java/org/elasticsearch/rest/RestFilterChainTests.java index 5013d637436d2..164e318dcf3a0 100644 --- a/core/src/test/java/org/elasticsearch/rest/RestFilterChainTests.java +++ b/core/src/test/java/org/elasticsearch/rest/RestFilterChainTests.java @@ -19,13 +19,17 @@ package org.elasticsearch.rest; +import org.elasticsearch.Version; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.LocalTransportAddress; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestChannel; import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.usage.UsageService; import java.util.ArrayList; import java.util.Collections; @@ -40,7 +44,9 @@ public class RestFilterChainTests extends ESTestCase { public void testRestFilters() throws Exception { - RestController restController = new RestController(Settings.EMPTY, Collections.emptySet()); + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, Settings.EMPTY); + RestController restController = new RestController(Settings.EMPTY, Collections.emptySet(), usageService); int numFilters = randomInt(10); Set orders = new HashSet<>(numFilters); @@ -121,7 +127,9 @@ public void testTooManyContinueProcessing() throws Exception { } }); - RestController restController = new RestController(Settings.EMPTY, Collections.emptySet()); + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, Settings.EMPTY); + RestController restController = new RestController(Settings.EMPTY, Collections.emptySet(), usageService); restController.registerFilter(testFilter); restController.registerHandler(RestRequest.Method.GET, "/", new RestHandler() { diff --git a/core/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java b/core/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java index 6fc0333fecf2c..d44562cb3779b 100644 --- a/core/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java +++ b/core/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.RecoverySource.PeerRecoverySource; import org.elasticsearch.cluster.routing.RecoverySource.StoreRecoverySource; import org.elasticsearch.cluster.routing.ShardRouting; @@ -37,6 +38,7 @@ import org.elasticsearch.common.Table; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.LocalTransportAddress; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.Index; import org.elasticsearch.index.cache.query.QueryCacheStats; @@ -57,6 +59,7 @@ import org.elasticsearch.rest.RestController; import org.elasticsearch.search.suggest.completion.CompletionStats; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.usage.UsageService; import java.nio.file.Path; import java.util.ArrayList; @@ -73,7 +76,9 @@ public class RestIndicesActionTests extends ESTestCase { public void testBuildTable() { final Settings settings = Settings.EMPTY; - final RestController restController = new RestController(settings, Collections.emptySet()); + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, settings); + final RestController restController = new RestController(settings, Collections.emptySet(), usageService); final RestIndicesAction action = new RestIndicesAction(settings, restController, new IndexNameExpressionResolver(settings)); // build a (semi-)random table diff --git a/core/src/test/java/org/elasticsearch/rest/action/cat/RestRecoveryActionTests.java b/core/src/test/java/org/elasticsearch/rest/action/cat/RestRecoveryActionTests.java index 9495ba7e995a0..8163706748bfb 100644 --- a/core/src/test/java/org/elasticsearch/rest/action/cat/RestRecoveryActionTests.java +++ b/core/src/test/java/org/elasticsearch/rest/action/cat/RestRecoveryActionTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.rest.action.cat; +import org.elasticsearch.Version; import org.elasticsearch.action.ShardOperationFailedException; import org.elasticsearch.action.admin.indices.recovery.RecoveryResponse; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -28,12 +29,14 @@ import org.elasticsearch.common.Randomness; import org.elasticsearch.common.Table; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.LocalTransportAddress; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.rest.RestController; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.usage.UsageService; import java.util.ArrayList; import java.util.Collections; @@ -50,7 +53,9 @@ public class RestRecoveryActionTests extends ESTestCase { public void testRestRecoveryAction() { final Settings settings = Settings.EMPTY; - final RestController restController = new RestController(settings, Collections.emptySet()); + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, settings); + final RestController restController = new RestController(settings, Collections.emptySet(), usageService); final RestRecoveryAction action = new RestRecoveryAction(settings, restController, restController); final int totalShards = randomIntBetween(1, 32); final int successfulShards = Math.max(0, totalShards - randomIntBetween(1, 2)); diff --git a/core/src/test/java/org/elasticsearch/usage/UsageServiceTests.java b/core/src/test/java/org/elasticsearch/usage/UsageServiceTests.java new file mode 100644 index 0000000000000..be23506abe0a6 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/usage/UsageServiceTests.java @@ -0,0 +1,100 @@ +/* + * 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.usage; + +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.node.usage.NodeUsage; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.LocalTransportAddress; +import org.elasticsearch.test.ESTestCase; + +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.sameInstance; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class UsageServiceTests extends ESTestCase { + + public void testRestUsage() { + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, Settings.EMPTY); + usageService.addRestCall("a"); + usageService.addRestCall("b"); + usageService.addRestCall("a"); + usageService.addRestCall("a"); + usageService.addRestCall("b"); + usageService.addRestCall("c"); + usageService.addRestCall("c"); + usageService.addRestCall("d"); + usageService.addRestCall("a"); + usageService.addRestCall("b"); + usageService.addRestCall("e"); + usageService.addRestCall("f"); + usageService.addRestCall("c"); + usageService.addRestCall("d"); + NodeUsage usage = usageService.getUsageStats(true); + assertThat(usage.getNode(), sameInstance(discoveryNode)); + Map restUsage = usage.getRestUsage(); + assertThat(restUsage, notNullValue()); + assertThat(restUsage.size(), equalTo(6)); + assertThat(restUsage.get("a"), equalTo(4L)); + assertThat(restUsage.get("b"), equalTo(3L)); + assertThat(restUsage.get("c"), equalTo(3L)); + assertThat(restUsage.get("d"), equalTo(2L)); + assertThat(restUsage.get("e"), equalTo(1L)); + assertThat(restUsage.get("f"), equalTo(1L)); + + usage = usageService.getUsageStats(false); + assertThat(usage.getNode(), sameInstance(discoveryNode)); + assertThat(usage.getRestUsage(), nullValue()); + } + + public void testClearUsage() { + DiscoveryNode discoveryNode = new DiscoveryNode("foo", new LocalTransportAddress("bar"), Version.CURRENT); + UsageService usageService = new UsageService(() -> discoveryNode, Settings.EMPTY); + usageService.addRestCall("a"); + usageService.addRestCall("b"); + usageService.addRestCall("c"); + usageService.addRestCall("d"); + usageService.addRestCall("e"); + usageService.addRestCall("f"); + NodeUsage usage = usageService.getUsageStats(true); + assertThat(usage.getNode(), sameInstance(discoveryNode)); + Map restUsage = usage.getRestUsage(); + assertThat(restUsage, notNullValue()); + assertThat(restUsage.size(), equalTo(6)); + assertThat(restUsage.get("a"), equalTo(1L)); + assertThat(restUsage.get("b"), equalTo(1L)); + assertThat(restUsage.get("c"), equalTo(1L)); + assertThat(restUsage.get("d"), equalTo(1L)); + assertThat(restUsage.get("e"), equalTo(1L)); + assertThat(restUsage.get("f"), equalTo(1L)); + + usageService.clear(); + usage = usageService.getUsageStats(true); + assertThat(usage.getNode(), sameInstance(discoveryNode)); + assertThat(usage.getRestUsage(), notNullValue()); + assertThat(usage.getRestUsage().size(), equalTo(0)); + } + +} diff --git a/distribution/integ-test-zip/src/test/java/org/elasticsearch/test/rest/NodeRestUsageIT.java b/distribution/integ-test-zip/src/test/java/org/elasticsearch/test/rest/NodeRestUsageIT.java new file mode 100644 index 0000000000000..0380a22394866 --- /dev/null +++ b/distribution/integ-test-zip/src/test/java/org/elasticsearch/test/rest/NodeRestUsageIT.java @@ -0,0 +1,206 @@ +/* + * 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.test.rest; + +import org.apache.http.entity.StringEntity; +import org.elasticsearch.client.Response; +import org.elasticsearch.rest.action.admin.cluster.RestNodesInfoAction; +import org.elasticsearch.rest.action.admin.cluster.RestNodesStatsAction; +import org.elasticsearch.rest.action.admin.cluster.RestNodesUsageAction; +import org.elasticsearch.rest.action.admin.indices.RestCreateIndexAction; +import org.elasticsearch.rest.action.admin.indices.RestDeleteIndexAction; +import org.elasticsearch.rest.action.admin.indices.RestRefreshAction; +import org.elasticsearch.rest.action.cat.RestIndicesAction; +import org.elasticsearch.rest.action.document.RestIndexAction; +import org.elasticsearch.rest.action.search.RestSearchAction; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class NodeRestUsageIT extends ESRestTestCase { + + @SuppressWarnings("unchecked") + public void testNoRestUsage() throws IOException { + Response response = client().performRequest("GET", "_nodes/usage"); + Map responseBodyMap = entityAsMap(response); + assertThat(responseBodyMap, notNullValue()); + Map _nodesMap = (Map) responseBodyMap.get("_nodes"); + assertThat(_nodesMap, notNullValue()); + Integer total = (Integer) _nodesMap.get("total"); + Integer successful = (Integer) _nodesMap.get("successful"); + Integer failed = (Integer) _nodesMap.get("failed"); + assertThat(total, greaterThan(0)); + assertThat(successful, equalTo(total)); + assertThat(failed, equalTo(0)); + + Map nodesMap = (Map) responseBodyMap.get("nodes"); + assertThat(nodesMap, notNullValue()); + assertThat(nodesMap.size(), equalTo(successful)); + Map combinedRestUsage = new HashMap<>(); + for (Map.Entry nodeEntry : nodesMap.entrySet()) { + Map restActionUsage = (Map) ((Map) nodeEntry.getValue()).get("rest_actions"); + assertThat(restActionUsage, notNullValue()); + for (Map.Entry restActionEntry : restActionUsage.entrySet()) { + Integer currentUsage = combinedRestUsage.get(restActionEntry.getKey()); + if (currentUsage == null) { + combinedRestUsage.put(restActionEntry.getKey(), (Integer) restActionEntry.getValue()); + } else { + combinedRestUsage.put(restActionEntry.getKey(), currentUsage + (Integer) restActionEntry.getValue()); + } + } + } + // The call to the nodes usage api counts in the stats so it should be + // the only thing returned in the response + assertThat(combinedRestUsage.size(), equalTo(1)); + assertThat(combinedRestUsage.get(RestNodesUsageAction.class.getName()), equalTo(1)); + + } + + @SuppressWarnings("unchecked") + public void testWithRestUsage() throws IOException { + // Do some requests to get some rest usage stats + client().performRequest("PUT", "/test"); + client().performRequest("POST", "/test/doc/1", Collections.emptyMap(), new StringEntity("{ \"foo\": \"bar\"}")); + client().performRequest("POST", "/test/doc/2", Collections.emptyMap(), new StringEntity("{ \"foo\": \"bar\"}")); + client().performRequest("POST", "/test/doc/3", Collections.emptyMap(), new StringEntity("{ \"foo\": \"bar\"}")); + client().performRequest("GET", "/test/_search"); + client().performRequest("POST", "/test/doc/4", Collections.emptyMap(), new StringEntity("{ \"foo\": \"bar\"}")); + client().performRequest("POST", "/test/_refresh"); + client().performRequest("GET", "/_cat/indices"); + client().performRequest("GET", "/_nodes"); + client().performRequest("GET", "/test/_search"); + client().performRequest("GET", "/_nodes/stats"); + client().performRequest("DELETE", "/test"); + + Response response = client().performRequest("GET", "_nodes/usage"); + Map responseBodyMap = entityAsMap(response); + assertThat(responseBodyMap, notNullValue()); + Map _nodesMap = (Map) responseBodyMap.get("_nodes"); + assertThat(_nodesMap, notNullValue()); + Integer total = (Integer) _nodesMap.get("total"); + Integer successful = (Integer) _nodesMap.get("successful"); + Integer failed = (Integer) _nodesMap.get("failed"); + assertThat(total, greaterThan(0)); + assertThat(successful, equalTo(total)); + assertThat(failed, equalTo(0)); + + Map nodesMap = (Map) responseBodyMap.get("nodes"); + assertThat(nodesMap, notNullValue()); + assertThat(nodesMap.size(), equalTo(successful)); + Map combinedRestUsage = new HashMap<>(); + for (Map.Entry nodeEntry : nodesMap.entrySet()) { + Map restActionUsage = (Map) ((Map) nodeEntry.getValue()).get("rest_actions"); + assertThat(restActionUsage, notNullValue()); + for (Map.Entry restActionEntry : restActionUsage.entrySet()) { + Integer currentUsage = combinedRestUsage.get(restActionEntry.getKey()); + if (currentUsage == null) { + combinedRestUsage.put(restActionEntry.getKey(), (Integer) restActionEntry.getValue()); + } else { + combinedRestUsage.put(restActionEntry.getKey(), currentUsage + (Integer) restActionEntry.getValue()); + } + } + } + // The call to the nodes usage api counts in the stats so it should be + // the only thing returned in the response + assertThat(combinedRestUsage.size(), equalTo(9)); + assertThat(combinedRestUsage.get(RestNodesUsageAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestCreateIndexAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestIndexAction.class.getName()), equalTo(4)); + assertThat(combinedRestUsage.get(RestSearchAction.class.getName()), equalTo(2)); + assertThat(combinedRestUsage.get(RestRefreshAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestIndicesAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestNodesInfoAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestNodesStatsAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestDeleteIndexAction.class.getName()), equalTo(1)); + + } + + @SuppressWarnings("unchecked") + public void testClearRestUsage() throws IOException { + // Do some requests to get some rest usage stats + client().performRequest("GET", "/_cat/indices"); + client().performRequest("GET", "/_nodes"); + client().performRequest("GET", "/_nodes/stats"); + + Response response = client().performRequest("GET", "_nodes/usage"); + Map responseBodyMap = entityAsMap(response); + assertThat(responseBodyMap, notNullValue()); + Map _nodesMap = (Map) responseBodyMap.get("_nodes"); + assertThat(_nodesMap, notNullValue()); + Integer total = (Integer) _nodesMap.get("total"); + Integer successful = (Integer) _nodesMap.get("successful"); + Integer failed = (Integer) _nodesMap.get("failed"); + assertThat(total, greaterThan(0)); + assertThat(successful, equalTo(total)); + assertThat(failed, equalTo(0)); + + Map nodesMap = (Map) responseBodyMap.get("nodes"); + assertThat(nodesMap, notNullValue()); + assertThat(nodesMap.size(), equalTo(successful)); + Map combinedRestUsage = new HashMap<>(); + for (Map.Entry nodeEntry : nodesMap.entrySet()) { + Map restActionUsage = (Map) ((Map) nodeEntry.getValue()).get("rest_actions"); + assertThat(restActionUsage, notNullValue()); + for (Map.Entry restActionEntry : restActionUsage.entrySet()) { + Integer currentUsage = combinedRestUsage.get(restActionEntry.getKey()); + if (currentUsage == null) { + combinedRestUsage.put(restActionEntry.getKey(), (Integer) restActionEntry.getValue()); + } else { + combinedRestUsage.put(restActionEntry.getKey(), currentUsage + (Integer) restActionEntry.getValue()); + } + } + } + // The call to the nodes usage api counts in the stats so it should be + // the only thing returned in the response + assertThat(combinedRestUsage.size(), equalTo(4)); + assertThat(combinedRestUsage.get(RestNodesUsageAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestIndicesAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestNodesInfoAction.class.getName()), equalTo(1)); + assertThat(combinedRestUsage.get(RestNodesStatsAction.class.getName()), equalTo(1)); + + response = client().performRequest("GET", "_nodes/usage/_clear"); + responseBodyMap = entityAsMap(response); + assertThat(responseBodyMap, notNullValue()); + _nodesMap = (Map) responseBodyMap.get("_nodes"); + assertThat(_nodesMap, notNullValue()); + total = (Integer) _nodesMap.get("total"); + successful = (Integer) _nodesMap.get("successful"); + failed = (Integer) _nodesMap.get("failed"); + assertThat(total, greaterThan(0)); + assertThat(successful, equalTo(total)); + assertThat(failed, equalTo(0)); + + nodesMap = (Map) responseBodyMap.get("nodes"); + assertThat(nodesMap, notNullValue()); + assertThat(nodesMap.size(), equalTo(successful)); + for (Map.Entry nodeEntry : nodesMap.entrySet()) { + Map restActionUsage = (Map) ((Map) nodeEntry.getValue()).get("rest_actions"); + assertThat(restActionUsage, nullValue()); + } + } + +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/nodes.usage.json b/rest-api-spec/src/main/resources/rest-api-spec/api/nodes.usage.json new file mode 100644 index 0000000000000..0e706b391c9aa --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/nodes.usage.json @@ -0,0 +1,38 @@ +{ + "nodes.usage": { + "documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-nodes-usage.html", + "methods": ["GET"], + "url": { + "path": "/_nodes/usage", + "paths": [ + "/_nodes/usage", + "/_nodes/{node_id}/usage", + "/_nodes/usage/{metric}", + "/_nodes/{node_id}/usage/{metric}" + ], + "parts": { + "metric" : { + "type" : "list", + "options" : ["_all", "rest_actions"], + "description" : "Limit the information returned to the specified metrics" + }, + "node_id": { + "type" : "list", + "description" : "A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes" + } + }, + "params": { + "human": { + "type": "boolean", + "description": "Whether to return time and byte values in human-readable format.", + "default": false + }, + "timeout": { + "type" : "time", + "description" : "Explicit operation timeout" + } + } + }, + "body": null + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 573c301105ad9..ca8d5edd161a5 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -120,9 +120,16 @@ public ESRestTestCase() { public final void after() throws Exception { wipeCluster(); logIfThereAreRunningTasks(); + // Need to wipe usage stats here as logIfThereAreRunningTasks performs + // requests + wipeUsageStats(); closeClients(); } + private void wipeUsageStats() throws IOException { + adminClient().performRequest("POST", "_nodes/usage/_clear"); + } + /** * Get a client, building it if it hasn't been built for this test. */ From 185462015351883b676e6fee59b720e6d379e7a0 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 24 Aug 2016 09:12:05 +0100 Subject: [PATCH 2/2] review comments --- .../java/org/elasticsearch/usage/UsageService.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/usage/UsageService.java b/core/src/main/java/org/elasticsearch/usage/UsageService.java index 514dca8ad9352..98d3698aade0c 100644 --- a/core/src/main/java/org/elasticsearch/usage/UsageService.java +++ b/core/src/main/java/org/elasticsearch/usage/UsageService.java @@ -29,7 +29,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; import java.util.function.Supplier; /** @@ -38,7 +38,7 @@ public class UsageService extends AbstractComponent { private final Supplier localNodeSupplier; - private final Map restUsage; + private final Map restUsage; private long sinceTime; @Inject @@ -57,8 +57,8 @@ public UsageService(Supplier localNodeSupplier, Settings settings * endpoint. */ public void addRestCall(String actionName) { - AtomicLong counter = restUsage.computeIfAbsent(actionName, key -> new AtomicLong()); - counter.getAndIncrement(); + LongAdder counter = restUsage.computeIfAbsent(actionName, key -> new LongAdder()); + counter.increment(); } public void clear() { @@ -80,7 +80,7 @@ public NodeUsage getUsageStats(boolean restActions) { if (restActions) { restUsageMap = new HashMap<>(); restUsage.forEach((key, value) -> { - restUsageMap.put(key, value.get()); + restUsageMap.put(key, value.longValue()); }); } else { restUsageMap = null;