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..98d3698aade0c --- /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.LongAdder; +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) { + LongAdder counter = restUsage.computeIfAbsent(actionName, key -> new LongAdder()); + counter.increment(); + } + + 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.longValue()); + }); + } 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. */