diff --git a/src/main/java/org/opensearch/plugin/insights/QueryInsightsPlugin.java b/src/main/java/org/opensearch/plugin/insights/QueryInsightsPlugin.java index e5f15ea..a105cfd 100644 --- a/src/main/java/org/opensearch/plugin/insights/QueryInsightsPlugin.java +++ b/src/main/java/org/opensearch/plugin/insights/QueryInsightsPlugin.java @@ -31,8 +31,11 @@ import org.opensearch.plugin.insights.core.listener.QueryInsightsListener; import org.opensearch.plugin.insights.core.metrics.OperationalMetricsCounter; import org.opensearch.plugin.insights.core.service.QueryInsightsService; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsAction; import org.opensearch.plugin.insights.rules.action.top_queries.TopQueriesAction; +import org.opensearch.plugin.insights.rules.resthandler.health_stats.RestHealthStatsAction; import org.opensearch.plugin.insights.rules.resthandler.top_queries.RestTopQueriesAction; +import org.opensearch.plugin.insights.rules.transport.health_stats.TransportHealthStatsAction; import org.opensearch.plugin.insights.rules.transport.top_queries.TransportTopQueriesAction; import org.opensearch.plugin.insights.settings.QueryCategorizationSettings; import org.opensearch.plugin.insights.settings.QueryInsightsSettings; @@ -110,12 +113,15 @@ public List getRestHandlers( final IndexNameExpressionResolver indexNameExpressionResolver, final Supplier nodesInCluster ) { - return List.of(new RestTopQueriesAction()); + return List.of(new RestTopQueriesAction(), new RestHealthStatsAction()); } @Override public List> getActions() { - return List.of(new ActionPlugin.ActionHandler<>(TopQueriesAction.INSTANCE, TransportTopQueriesAction.class)); + return List.of( + new ActionPlugin.ActionHandler<>(TopQueriesAction.INSTANCE, TransportTopQueriesAction.class), + new ActionPlugin.ActionHandler<>(HealthStatsAction.INSTANCE, TransportHealthStatsAction.class) + ); } @Override diff --git a/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsAction.java b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsAction.java new file mode 100644 index 0000000..5b8108c --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsAction.java @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import org.opensearch.action.ActionType; + +/** + * Transport action for cluster/node level query insights plugin health stats. + */ +public class HealthStatsAction extends ActionType { + + /** + * The HealthStatsAction Instance. + */ + public static final HealthStatsAction INSTANCE = new HealthStatsAction(); + /** + * The name of this Action + */ + public static final String NAME = "cluster:admin/opensearch/insights/health_stats"; + + private HealthStatsAction() { + super(NAME, HealthStatsResponse::new); + } +} diff --git a/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsNodeResponse.java b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsNodeResponse.java new file mode 100644 index 0000000..40cfa1b --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsNodeResponse.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import java.io.IOException; +import org.opensearch.action.support.nodes.BaseNodeResponse; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.plugin.insights.rules.model.healthStats.QueryInsightsHealthStats; + +/** + * Holds the health stats retrieved from a node + */ +public class HealthStatsNodeResponse extends BaseNodeResponse implements ToXContentObject { + /** The health stats retrieved from one node */ + private final QueryInsightsHealthStats healthStats; + + /** + * Create the HealthStatsNodeResponse Object from StreamInput + * @param in A {@link StreamInput} object. + * @throws IOException IOException + */ + public HealthStatsNodeResponse(final StreamInput in) throws IOException { + super(in); + healthStats = new QueryInsightsHealthStats(in); + } + + /** + * Create the HealthStatsNodeResponse Object + * @param node A node that is part of the cluster. + * @param healthStats A list of HealthStats from nodes. + */ + public HealthStatsNodeResponse(final DiscoveryNode node, final QueryInsightsHealthStats healthStats) { + super(node); + this.healthStats = healthStats; + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(this.getNode().getId()); + healthStats.toXContent(builder, params); + return builder.endObject(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + healthStats.writeTo(out); + + } + + /** + * Get health stats + * + * @return the health stats records in this node response + */ + public QueryInsightsHealthStats getHealthStats() { + return healthStats; + } +} diff --git a/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsRequest.java b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsRequest.java new file mode 100644 index 0000000..f2a02a2 --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsRequest.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import java.io.IOException; +import org.opensearch.action.support.nodes.BaseNodesRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * A request to get cluster/node level health stats information. + */ +public class HealthStatsRequest extends BaseNodesRequest { + /** + * Constructor for HealthStatsRequest + * + * @param in A {@link StreamInput} object. + * @throws IOException if the stream cannot be deserialized. + */ + public HealthStatsRequest(final StreamInput in) throws IOException { + super(in); + } + + /** + * Get health stats from nodes based on the nodes ids specified. + * If none are passed, cluster level health stats will be returned. + * + * @param nodesIds the nodeIds specified in the request + */ + public HealthStatsRequest(final String... nodesIds) { + super(nodesIds); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + } +} diff --git a/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsResponse.java b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsResponse.java new file mode 100644 index 0000000..2e3a6c6 --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsResponse.java @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import java.io.IOException; +import java.util.List; +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.nodes.BaseNodesResponse; +import org.opensearch.cluster.ClusterName; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Transport response for cluster/node level health stats + */ +public class HealthStatsResponse extends BaseNodesResponse implements ToXContentFragment { + /** + * Constructor for HealthStatsNodeResponseResponse. + * + * @param in A {@link StreamInput} object. + * @throws IOException if the stream cannot be deserialized. + */ + public HealthStatsResponse(final StreamInput in) throws IOException { + super(in); + } + + /** + * Constructor for HealthStatsResponse + * + * @param clusterName The current cluster name + * @param nodes A list that contains health stats from all nodes + * @param failures A list that contains FailedNodeException + */ + public HealthStatsResponse( + final ClusterName clusterName, + final List nodes, + final List failures + ) { + super(clusterName, nodes, failures); + } + + @Override + protected List readNodesFrom(final StreamInput in) throws IOException { + return in.readList(HealthStatsNodeResponse::new); + } + + @Override + protected void writeNodesTo(final StreamOutput out, final List nodes) throws IOException { + out.writeList(nodes); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + final List results = getNodes(); + builder.startObject(); + for (HealthStatsNodeResponse nodeResponse : results) { + nodeResponse.toXContent(builder, params); + } + return builder.endObject(); + } + + @Override + public String toString() { + try { + final XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + builder.startObject(); + this.toXContent(builder, EMPTY_PARAMS); + builder.endObject(); + return builder.toString(); + } catch (IOException e) { + return "{ \"error\" : \"" + e.getMessage() + "\"}"; + } + } +} diff --git a/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/package-info.java b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/package-info.java new file mode 100644 index 0000000..0ecd172 --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/action/health_stats/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Transport Actions, Requests and Responses for Query Insights Health Stats + */ +package org.opensearch.plugin.insights.rules.action.health_stats; diff --git a/src/main/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/RestHealthStatsAction.java b/src/main/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/RestHealthStatsAction.java new file mode 100644 index 0000000..e07f6c8 --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/RestHealthStatsAction.java @@ -0,0 +1,81 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.resthandler.health_stats; + +import static org.opensearch.plugin.insights.settings.QueryInsightsSettings.QUERY_INSIGHTS_HEALTH_STATS_URI; +import static org.opensearch.rest.RestRequest.Method.GET; + +import java.util.List; +import java.util.Set; +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsAction; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsRequest; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsResponse; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestResponse; +import org.opensearch.rest.action.RestResponseListener; + +/** + * Rest action to get operational health stats of the query insights plugin + */ +public class RestHealthStatsAction extends BaseRestHandler { + /** + * Constructor for RestHealthStatsAction + */ + public RestHealthStatsAction() {} + + @Override + public List routes() { + return List.of(new Route(GET, QUERY_INSIGHTS_HEALTH_STATS_URI)); + } + + @Override + public String getName() { + return "query_insights_health_stats_action"; + } + + @Override + public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) { + final HealthStatsRequest healthStatsRequest = prepareRequest(request); + healthStatsRequest.timeout(request.param("timeout")); + + return channel -> client.execute(HealthStatsAction.INSTANCE, healthStatsRequest, healthStatsResponse(channel)); + } + + static HealthStatsRequest prepareRequest(final RestRequest request) { + final String[] nodesIds = Strings.splitStringByCommaToArray(request.param("nodeId")); + return new HealthStatsRequest(nodesIds); + } + + @Override + protected Set responseParams() { + return Settings.FORMAT_PARAMS; + } + + @Override + public boolean canTripCircuitBreaker() { + return false; + } + + RestResponseListener healthStatsResponse(final RestChannel channel) { + return new RestResponseListener<>(channel) { + @Override + public RestResponse buildResponse(final HealthStatsResponse response) throws Exception { + return new BytesRestResponse(RestStatus.OK, response.toXContent(channel.newBuilder(), ToXContent.EMPTY_PARAMS)); + } + }; + } +} diff --git a/src/main/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/package-info.java b/src/main/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/package-info.java new file mode 100644 index 0000000..eba376e --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Rest Handlers for Query Insights Health Stats + */ +package org.opensearch.plugin.insights.rules.resthandler.health_stats; diff --git a/src/main/java/org/opensearch/plugin/insights/rules/transport/health_stats/TransportHealthStatsAction.java b/src/main/java/org/opensearch/plugin/insights/rules/transport/health_stats/TransportHealthStatsAction.java new file mode 100644 index 0000000..77520a9 --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/transport/health_stats/TransportHealthStatsAction.java @@ -0,0 +1,128 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.transport.health_stats; + +import java.io.IOException; +import java.util.List; +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.nodes.BaseNodeRequest; +import org.opensearch.action.support.nodes.TransportNodesAction; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.plugin.insights.core.service.QueryInsightsService; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsAction; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsNodeResponse; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsRequest; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsResponse; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; + +/** + * Transport action for cluster/node level health stats information. + */ +public class TransportHealthStatsAction extends TransportNodesAction< + HealthStatsRequest, + HealthStatsResponse, + TransportHealthStatsAction.NodeRequest, + HealthStatsNodeResponse> { + + private final QueryInsightsService queryInsightsService; + + /** + * Create the TransportHealthStatsAction Object + + * @param threadPool The OpenSearch thread pool to run async tasks + * @param clusterService The clusterService of this node + * @param transportService The TransportService of this node + * @param queryInsightsService The queryInsightsService associated with this Transport Action + * @param actionFilters the action filters + */ + @Inject + public TransportHealthStatsAction( + final ThreadPool threadPool, + final ClusterService clusterService, + final TransportService transportService, + final QueryInsightsService queryInsightsService, + final ActionFilters actionFilters + ) { + super( + HealthStatsAction.NAME, + threadPool, + clusterService, + transportService, + actionFilters, + HealthStatsRequest::new, + NodeRequest::new, + ThreadPool.Names.GENERIC, + HealthStatsNodeResponse.class + ); + this.queryInsightsService = queryInsightsService; + } + + @Override + protected HealthStatsResponse newResponse( + final HealthStatsRequest healthStatsRequest, + final List responses, + final List failures + ) { + return new HealthStatsResponse(clusterService.getClusterName(), responses, failures); + } + + @Override + protected NodeRequest newNodeRequest(final HealthStatsRequest request) { + return new NodeRequest(request); + } + + @Override + protected HealthStatsNodeResponse newNodeResponse(final StreamInput in) throws IOException { + return new HealthStatsNodeResponse(in); + } + + @Override + protected HealthStatsNodeResponse nodeOperation(final NodeRequest nodeRequest) { + final HealthStatsRequest request = nodeRequest.request; + return new HealthStatsNodeResponse(clusterService.localNode(), queryInsightsService.getHealthStats()); + } + + /** + * Inner Node Health Check Request + */ + public static class NodeRequest extends BaseNodeRequest { + + final HealthStatsRequest request; + + /** + * Create the NodeResponse object from StreamInput + * + * @param in the StreamInput to read the object + * @throws IOException IOException + */ + public NodeRequest(StreamInput in) throws IOException { + super(in); + request = new HealthStatsRequest(in); + } + + /** + * Create the NodeResponse object from a HealthStatsRequest + * @param request the HealthStatsRequest object + */ + public NodeRequest(final HealthStatsRequest request) { + this.request = request; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + request.writeTo(out); + } + } +} diff --git a/src/main/java/org/opensearch/plugin/insights/rules/transport/health_stats/package-info.java b/src/main/java/org/opensearch/plugin/insights/rules/transport/health_stats/package-info.java new file mode 100644 index 0000000..846391d --- /dev/null +++ b/src/main/java/org/opensearch/plugin/insights/rules/transport/health_stats/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Transport Actions for Health Stats. + */ +package org.opensearch.plugin.insights.rules.transport.health_stats; diff --git a/src/main/java/org/opensearch/plugin/insights/settings/QueryInsightsSettings.java b/src/main/java/org/opensearch/plugin/insights/settings/QueryInsightsSettings.java index d38f81d..8bf03a8 100644 --- a/src/main/java/org/opensearch/plugin/insights/settings/QueryInsightsSettings.java +++ b/src/main/java/org/opensearch/plugin/insights/settings/QueryInsightsSettings.java @@ -74,6 +74,7 @@ public class QueryInsightsSettings { public static final int DEFAULT_GROUPS_EXCLUDING_TOPN_LIMIT = 100; public static final int MAX_GROUPS_EXCLUDING_TOPN_LIMIT = 10000; + public static final String QUERY_INSIGHTS_HEALTH_STATS_URI = PLUGINS_BASE_URI + "/health_stats"; /** * Settings for Top Queries diff --git a/src/test/java/org/opensearch/plugin/insights/QueryInsightsPluginTests.java b/src/test/java/org/opensearch/plugin/insights/QueryInsightsPluginTests.java index fca21e5..2d2c1dc 100644 --- a/src/test/java/org/opensearch/plugin/insights/QueryInsightsPluginTests.java +++ b/src/test/java/org/opensearch/plugin/insights/QueryInsightsPluginTests.java @@ -23,7 +23,9 @@ import org.opensearch.core.action.ActionResponse; import org.opensearch.plugin.insights.core.listener.QueryInsightsListener; import org.opensearch.plugin.insights.core.service.QueryInsightsService; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsAction; import org.opensearch.plugin.insights.rules.action.top_queries.TopQueriesAction; +import org.opensearch.plugin.insights.rules.resthandler.health_stats.RestHealthStatsAction; import org.opensearch.plugin.insights.rules.resthandler.top_queries.RestTopQueriesAction; import org.opensearch.plugin.insights.settings.QueryCategorizationSettings; import org.opensearch.plugin.insights.settings.QueryInsightsSettings; @@ -114,14 +116,16 @@ public void testGetExecutorBuilders() { public void testGetRestHandlers() { List components = queryInsightsPlugin.getRestHandlers(Settings.EMPTY, null, null, null, null, null, null); - assertEquals(1, components.size()); + assertEquals(2, components.size()); assertTrue(components.get(0) instanceof RestTopQueriesAction); + assertTrue(components.get(1) instanceof RestHealthStatsAction); } public void testGetActions() { List> components = queryInsightsPlugin.getActions(); - assertEquals(1, components.size()); + assertEquals(2, components.size()); assertTrue(components.get(0).getAction() instanceof TopQueriesAction); + assertTrue(components.get(1).getAction() instanceof HealthStatsAction); } } diff --git a/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsActionTests.java b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsActionTests.java new file mode 100644 index 0000000..4ccb95e --- /dev/null +++ b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsActionTests.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import org.opensearch.test.OpenSearchTestCase; + +/** + * Unit tests for the {@link HealthStatsAction} class. + */ +public class HealthStatsActionTests extends OpenSearchTestCase { + + public void testSingletonInstance() { + assertNotNull(HealthStatsAction.INSTANCE); + } + + public void testActionName() { + assertEquals("cluster:admin/opensearch/insights/health_stats", HealthStatsAction.NAME); + assertEquals(HealthStatsAction.NAME, HealthStatsAction.INSTANCE.name()); + } + + public void testActionResponse() { + // Verify that the response type supplier produces a HealthStatsResponse object + assertNotNull(HealthStatsAction.INSTANCE.getResponseReader()); + } +} diff --git a/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsNodeResponseTests.java b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsNodeResponseTests.java new file mode 100644 index 0000000..4185a4c --- /dev/null +++ b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsNodeResponseTests.java @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; + +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.plugin.insights.rules.model.healthStats.QueryInsightsHealthStats; +import org.opensearch.plugin.insights.settings.QueryInsightsSettings; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.test.VersionUtils; +import org.opensearch.threadpool.ScalingExecutorBuilder; +import org.opensearch.threadpool.TestThreadPool; +import org.opensearch.threadpool.ThreadPool; + +/** + * Unit tests for the {@link HealthStatsNodeResponse} class. + */ +public class HealthStatsNodeResponseTests extends OpenSearchTestCase { + private ThreadPool threadPool; + private final DiscoveryNode discoveryNode = new DiscoveryNode( + "node_for_health_stats_test", + buildNewFakeTransportAddress(), + emptyMap(), + emptySet(), + VersionUtils.randomVersion(random()) + ); + private QueryInsightsHealthStats healthStats; + + @Before + public void setup() { + this.threadPool = new TestThreadPool( + "QueryInsightsHealthStatsTests", + new ScalingExecutorBuilder(QueryInsightsSettings.QUERY_INSIGHTS_EXECUTOR, 1, 5, TimeValue.timeValueMinutes(5)) + ); + this.healthStats = new QueryInsightsHealthStats( + threadPool.info(QueryInsightsSettings.QUERY_INSIGHTS_EXECUTOR), + 10, + new HashMap<>() + ); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); + } + + public void testConstructorWithNodeAndStats() { + HealthStatsNodeResponse response = new HealthStatsNodeResponse(discoveryNode, healthStats); + // Verify that the object is correctly initialized + assertEquals(discoveryNode, response.getNode()); + assertEquals(healthStats, response.getHealthStats()); + } + + public void testConstructorWithStreamInput() throws IOException { + // Serialize the HealthStatsNodeResponse to a StreamOutput + HealthStatsNodeResponse originalResponse = new HealthStatsNodeResponse(discoveryNode, healthStats); + BytesStreamOutput out = new BytesStreamOutput(); + originalResponse.writeTo(out); + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + HealthStatsNodeResponse response = new HealthStatsNodeResponse(in); + assertNotNull(response); + assertEquals(originalResponse.getHealthStats().getQueryRecordsQueueSize(), response.getHealthStats().getQueryRecordsQueueSize()); + assertEquals(originalResponse.getNode(), response.getNode()); + } + + public void testWriteTo() throws IOException { + HealthStatsNodeResponse response = new HealthStatsNodeResponse(discoveryNode, healthStats); + BytesStreamOutput out = new BytesStreamOutput(); + response.writeTo(out); + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + HealthStatsNodeResponse deserializedResponse = new HealthStatsNodeResponse(in); + assertEquals( + response.getHealthStats().getQueryRecordsQueueSize(), + deserializedResponse.getHealthStats().getQueryRecordsQueueSize() + ); + assertEquals(response.getNode(), deserializedResponse.getNode()); + } + + public void testToXContent() throws IOException { + HealthStatsNodeResponse response = new HealthStatsNodeResponse(discoveryNode, healthStats); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + String jsonString = builder.toString(); + assertNotNull(jsonString); + assertTrue(jsonString.contains(discoveryNode.getId())); + } + + public void testGetHealthStats() { + HealthStatsNodeResponse response = new HealthStatsNodeResponse(discoveryNode, healthStats); + assertEquals(healthStats, response.getHealthStats()); + } +} diff --git a/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsRequestTests.java b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsRequestTests.java new file mode 100644 index 0000000..bdfae5e --- /dev/null +++ b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsRequestTests.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import java.io.IOException; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.test.OpenSearchTestCase; + +/** + * Unit tests for the {@link HealthStatsRequest} class. + */ +public class HealthStatsRequestTests extends OpenSearchTestCase { + + public void testConstructorWithNodeIds() { + String[] nodeIds = { "node1", "node2" }; + HealthStatsRequest request = new HealthStatsRequest(nodeIds); + assertNotNull(request); + assertArrayEquals(nodeIds, request.nodesIds()); + } + + public void testConstructorWithStreamInput() throws IOException { + String[] nodeIds = { "node1", "node2" }; + HealthStatsRequest originalRequest = new HealthStatsRequest(nodeIds); + BytesStreamOutput out = new BytesStreamOutput(); + originalRequest.writeTo(out); + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + HealthStatsRequest requestFromStream = new HealthStatsRequest(in); + assertNotNull(requestFromStream); + assertArrayEquals(nodeIds, requestFromStream.nodesIds()); + } + + public void testWriteTo() throws IOException { + String[] nodeIds = { "node1", "node2" }; + HealthStatsRequest request = new HealthStatsRequest(nodeIds); + BytesStreamOutput out = new BytesStreamOutput(); + request.writeTo(out); + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + HealthStatsRequest deserializedRequest = new HealthStatsRequest(in); + assertNotNull(deserializedRequest); + assertArrayEquals(nodeIds, deserializedRequest.nodesIds()); + } +} diff --git a/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsResponseTests.java b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsResponseTests.java new file mode 100644 index 0000000..28ca22a --- /dev/null +++ b/src/test/java/org/opensearch/plugin/insights/rules/action/health_stats/HealthStatsResponseTests.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.action.health_stats; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.opensearch.action.FailedNodeException; +import org.opensearch.cluster.ClusterName; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.plugin.insights.rules.model.healthStats.QueryInsightsHealthStats; +import org.opensearch.plugin.insights.settings.QueryInsightsSettings; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.test.VersionUtils; +import org.opensearch.threadpool.ScalingExecutorBuilder; +import org.opensearch.threadpool.TestThreadPool; +import org.opensearch.threadpool.ThreadPool; + +/** + * Unit tests for the {@link HealthStatsResponse} class. + */ +public class HealthStatsResponseTests extends OpenSearchTestCase { + private ThreadPool threadPool; + private final DiscoveryNode discoveryNode = new DiscoveryNode( + "node_for_health_stats_test", + buildNewFakeTransportAddress(), + emptyMap(), + emptySet(), + VersionUtils.randomVersion(random()) + ); + private QueryInsightsHealthStats healthStats; + + @Before + public void setup() { + this.threadPool = new TestThreadPool( + "QueryInsightsHealthStatsTests", + new ScalingExecutorBuilder(QueryInsightsSettings.QUERY_INSIGHTS_EXECUTOR, 1, 5, TimeValue.timeValueMinutes(5)) + ); + this.healthStats = new QueryInsightsHealthStats( + threadPool.info(QueryInsightsSettings.QUERY_INSIGHTS_EXECUTOR), + 10, + new HashMap<>() + ); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); + } + + public void testConstructorWithClusterNameAndNodes() { + ClusterName clusterName = new ClusterName("test-cluster"); + List nodes = new ArrayList<>(); + List failures = new ArrayList<>(); + HealthStatsResponse response = new HealthStatsResponse(clusterName, nodes, failures); + assertEquals(clusterName, response.getClusterName()); + assertEquals(nodes, response.getNodes()); + assertEquals(failures, response.failures()); + } + + public void testConstructorWithStreamInput() throws IOException { + ClusterName clusterName = new ClusterName("test-cluster"); + List nodes = new ArrayList<>(); + List failures = new ArrayList<>(); + HealthStatsResponse originalResponse = new HealthStatsResponse(clusterName, nodes, failures); + BytesStreamOutput out = new BytesStreamOutput(); + originalResponse.writeTo(out); + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + HealthStatsResponse deserializedResponse = new HealthStatsResponse(in); + assertEquals(originalResponse.getClusterName(), deserializedResponse.getClusterName()); + assertEquals(originalResponse.getNodes(), deserializedResponse.getNodes()); + } + + public void testWriteNodesToAndReadNodesFrom() throws IOException { + ClusterName clusterName = new ClusterName("test-cluster"); + List nodes = Collections.emptyList(); + List failures = Collections.emptyList(); + HealthStatsResponse response = new HealthStatsResponse(clusterName, nodes, failures); + BytesStreamOutput out = new BytesStreamOutput(); + // Serialize nodes + response.writeTo(out); + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + // Deserialize nodes + HealthStatsResponse deserializedResponse = new HealthStatsResponse(in); + assertEquals(response.getNodes().size(), deserializedResponse.getNodes().size()); + } + + public void testToXContent() throws IOException { + ClusterName clusterName = new ClusterName("test-cluster"); + List nodes = List.of(new HealthStatsNodeResponse(discoveryNode, healthStats)); + List failures = Collections.emptyList(); + HealthStatsResponse response = new HealthStatsResponse(clusterName, nodes, failures); + XContentBuilder builder = XContentFactory.jsonBuilder(); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + String expectedJson = + "{\"node_for_health_stats_test\":{\"ThreadPoolInfo\":{\"query_insights_executor\":{\"type\":\"scaling\",\"core\":1,\"max\":5,\"keep_alive\":\"5m\",\"queue_size\":-1}},\"QueryRecordsQueueSize\":10,\"TopQueriesHealthStats\":{}}}"; + assertEquals(expectedJson, builder.toString()); + } +} diff --git a/src/test/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/RestHealthStatsActionTests.java b/src/test/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/RestHealthStatsActionTests.java new file mode 100644 index 0000000..842da15 --- /dev/null +++ b/src/test/java/org/opensearch/plugin/insights/rules/resthandler/health_stats/RestHealthStatsActionTests.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.resthandler.health_stats; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsRequest; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.test.rest.FakeRestRequest; + +public class RestHealthStatsActionTests extends OpenSearchTestCase { + + private RestHealthStatsAction restHealthStatsAction; + + @Before + public void setUp() throws Exception { + super.setUp(); + restHealthStatsAction = new RestHealthStatsAction(); + } + + public void testRoutes() { + List routes = restHealthStatsAction.routes(); + assertEquals(1, routes.size()); + assertEquals("GET", routes.get(0).getMethod().name()); + assertEquals("/_insights/health_stats", routes.get(0).getPath()); + } + + public void testGetName() { + assertEquals("query_insights_health_stats_action", restHealthStatsAction.getName()); + } + + public void testPrepareRequest() { + RestRequest request = buildRestRequest(Collections.singletonMap("nodeId", "node1,node2")); + HealthStatsRequest healthStatsRequest = RestHealthStatsAction.prepareRequest(request); + assertEquals(2, healthStatsRequest.nodesIds().length); + assertEquals("node1", healthStatsRequest.nodesIds()[0]); + assertEquals("node2", healthStatsRequest.nodesIds()[1]); + } + + public void testPrepareRequestWithEmptyNodeId() { + RestRequest request = buildRestRequest(Collections.emptyMap()); + HealthStatsRequest healthStatsRequest = RestHealthStatsAction.prepareRequest(request); + assertEquals(0, healthStatsRequest.nodesIds().length); + } + + private FakeRestRequest buildRestRequest(Map params) { + return new FakeRestRequest.Builder(xContentRegistry()).withMethod(RestRequest.Method.GET) + .withPath("/_insights/health_stats") + .withParams(params) + .build(); + } +} diff --git a/src/test/java/org/opensearch/plugin/insights/rules/transport/health_stats/TransportHealthStatsActionTests.java b/src/test/java/org/opensearch/plugin/insights/rules/transport/health_stats/TransportHealthStatsActionTests.java new file mode 100644 index 0000000..788cf45 --- /dev/null +++ b/src/test/java/org/opensearch/plugin/insights/rules/transport/health_stats/TransportHealthStatsActionTests.java @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.insights.rules.transport.health_stats; + +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.util.List; +import org.junit.Before; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.plugin.insights.core.service.QueryInsightsService; +import org.opensearch.plugin.insights.rules.action.health_stats.HealthStatsRequest; +import org.opensearch.plugin.insights.rules.action.top_queries.TopQueriesRequest; +import org.opensearch.plugin.insights.rules.action.top_queries.TopQueriesResponse; +import org.opensearch.plugin.insights.rules.model.MetricType; +import org.opensearch.plugin.insights.rules.transport.top_queries.TransportTopQueriesAction; +import org.opensearch.plugin.insights.settings.QueryInsightsSettings; +import org.opensearch.test.ClusterServiceUtils; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; + +public class TransportHealthStatsActionTests extends OpenSearchTestCase { + + private final ThreadPool threadPool = mock(ThreadPool.class); + + private final Settings.Builder settingsBuilder = Settings.builder(); + private final Settings settings = settingsBuilder.build(); + private final ClusterSettings clusterSettings = new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + private final ClusterService clusterService = ClusterServiceUtils.createClusterService(settings, clusterSettings, threadPool); + private final TransportService transportService = mock(TransportService.class); + private final QueryInsightsService queryInsightsService = mock(QueryInsightsService.class); + private final ActionFilters actionFilters = mock(ActionFilters.class); + private final TransportHealthStatsAction transportHealthStatsAction = new TransportHealthStatsAction( + threadPool, + clusterService, + transportService, + queryInsightsService, + actionFilters + ); + private final DummyParentAction dummyParentAction = new DummyParentAction( + threadPool, + clusterService, + transportService, + queryInsightsService, + actionFilters + ); + + class DummyParentAction extends TransportTopQueriesAction { + public DummyParentAction( + ThreadPool threadPool, + ClusterService clusterService, + TransportService transportService, + QueryInsightsService topQueriesByLatencyService, + ActionFilters actionFilters + ) { + super(threadPool, clusterService, transportService, topQueriesByLatencyService, actionFilters); + } + + public TopQueriesResponse createNewResponse() { + TopQueriesRequest request = new TopQueriesRequest(MetricType.LATENCY, null, null); + return newResponse(request, List.of(), List.of()); + } + } + + @Before + public void setup() { + clusterSettings.registerSetting(QueryInsightsSettings.TOP_N_LATENCY_QUERIES_ENABLED); + clusterSettings.registerSetting(QueryInsightsSettings.TOP_N_LATENCY_QUERIES_SIZE); + clusterSettings.registerSetting(QueryInsightsSettings.TOP_N_LATENCY_QUERIES_WINDOW_SIZE); + } + + public void testNewResponse() { + TopQueriesResponse response = dummyParentAction.createNewResponse(); + assertNotNull(response); + } + + public void testNewNodeRequest() { + HealthStatsRequest request = new HealthStatsRequest(); + TransportHealthStatsAction.NodeRequest nodeRequest = transportHealthStatsAction.newNodeRequest(request); + assertEquals(request, nodeRequest.request); + } + + public void testNodeRequestConstructor() throws IOException { + HealthStatsRequest request = new HealthStatsRequest(); + TransportHealthStatsAction.NodeRequest nodeRequest = new TransportHealthStatsAction.NodeRequest(request); + BytesStreamOutput out = new BytesStreamOutput(); + nodeRequest.writeTo(out); + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + TransportHealthStatsAction.NodeRequest deserializedRequest = new TransportHealthStatsAction.NodeRequest(in); + assertEquals(request.nodesIds().length, deserializedRequest.request.nodesIds().length); + } + +}