Skip to content

Commit

Permalink
SOLR-8207: Add "Nodes" view to the Admin UI "Cloud" tab, listing node…
Browse files Browse the repository at this point in the history
…s and key metrics
  • Loading branch information
janhoy committed Aug 3, 2018
1 parent 1af7686 commit 17a02c1
Show file tree
Hide file tree
Showing 16 changed files with 1,103 additions and 29 deletions.
2 changes: 2 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ New Features

* SOLR-12536: autoscaling policy support to equally distribute replicas on the basis of arbitrary properties (noble)

* SOLR-8207: Add "Nodes" view to the Admin UI "Cloud" tab, listing nodes and key metrics (janhoy)

* SOLR-11990: Make it possible to co-locate replicas of multiple collections together in a node. A collection may be
co-located with another collection during collection creation time by specifying a 'withCollection' parameter. It can
also be co-located afterwards by using the modify collection API. The co-location guarantee is enforced regardless of
Expand Down
2 changes: 1 addition & 1 deletion solr/core/src/java/org/apache/solr/core/CoreContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ public void load() {
configSetsHandler = createHandler(CONFIGSETS_HANDLER_PATH, cfg.getConfigSetsHandlerClass(), ConfigSetsHandler.class);

// metricsHistoryHandler uses metricsHandler, so create it first
metricsHandler = new MetricsHandler(metricManager);
metricsHandler = new MetricsHandler(this);
containerHandlers.put(METRICS_PATH, metricsHandler);
metricsHandler.initializeMetrics(metricManager, SolrInfoBean.Group.node.toString(), metricTag, METRICS_PATH);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.solr.handler.admin;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.Pair;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Static methods to proxy calls to an Admin (GET) API to other nodes in the cluster and return a combined response
*/
public class AdminHandlersProxy {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String PARAM_NODES = "nodes";

// Proxy this request to a different remote node if 'node' parameter is provided
public static boolean maybeProxyToNodes(SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer container)
throws IOException, SolrServerException, InterruptedException {
String nodeNames = req.getParams().get(PARAM_NODES);
if (nodeNames == null || nodeNames.isEmpty()) {
return false; // local request
}

if (!container.isZooKeeperAware()) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Parameter " + PARAM_NODES + " only supported in Cloud mode");
}

Set<String> nodes;
String pathStr = req.getPath();

Map<String,String> paramsMap = req.getParams().toMap(new HashMap<>());
paramsMap.remove(PARAM_NODES);
SolrParams params = new MapSolrParams(paramsMap);
Set<String> liveNodes = container.getZkController().zkStateReader.getClusterState().getLiveNodes();

if (nodeNames.equals("all")) {
nodes = liveNodes;
log.debug("All live nodes requested");
} else {
nodes = new HashSet<>(Arrays.asList(nodeNames.split(",")));
for (String nodeName : nodes) {
if (!nodeName.matches("^[^/:]+:\\d+_[\\w/]+$")) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Parameter " + PARAM_NODES + " has wrong format");
}

if (!liveNodes.contains(nodeName)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Requested node " + nodeName + " is not part of cluster");
}
}
log.debug("Nodes requested: {}", nodes);
}
log.debug(PARAM_NODES + " parameter {} specified on {} request", nodeNames, pathStr);

Map<String, Pair<Future<NamedList<Object>>, SolrClient>> responses = new HashMap<>();
for (String node : nodes) {
responses.put(node, callRemoteNode(node, pathStr, params, container.getZkController()));
}

for (Map.Entry<String, Pair<Future<NamedList<Object>>, SolrClient>> entry : responses.entrySet()) {
try {
NamedList<Object> resp = entry.getValue().first().get(10, TimeUnit.SECONDS);
entry.getValue().second().close();
rsp.add(entry.getKey(), resp);
} catch (ExecutionException ee) {
log.warn("Exception when fetching result from node {}", entry.getKey(), ee);
} catch (TimeoutException te) {
log.warn("Timeout when fetching result from node {}", entry.getKey(), te);
}
}
log.info("Fetched response from {} nodes: {}", responses.keySet().size(), responses.keySet());
return true;
}

/**
* Makes a remote request and returns a future and the solr client. The caller is responsible for closing the client
*/
public static Pair<Future<NamedList<Object>>, SolrClient> callRemoteNode(String nodeName, String endpoint,
SolrParams params, ZkController zkController)
throws IOException, SolrServerException {
log.debug("Proxying {} request to node {}", endpoint, nodeName);
URL baseUrl = new URL(zkController.zkStateReader.getBaseUrlForNodeName(nodeName));
HttpSolrClient solr = new HttpSolrClient.Builder(baseUrl.toString()).build();
SolrRequest proxyReq = new GenericSolrRequest(SolrRequest.METHOD.GET, endpoint, params);
HttpSolrClient.HttpUriRequestResponse proxyResp = solr.httpUriRequest(proxyReq);
return new Pair<>(proxyResp.future, solr);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.request.SolrQueryRequest;
Expand All @@ -67,11 +68,17 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName
public static final String ALL = "all";

private static final Pattern KEY_REGEX = Pattern.compile("(?<!" + Pattern.quote("\\") + ")" + Pattern.quote(":"));
private CoreContainer cc;

public MetricsHandler() {
this.metricManager = null;
}

public MetricsHandler(CoreContainer coreContainer) {
this.metricManager = coreContainer.getMetricManager();
this.cc = coreContainer;
}

public MetricsHandler(SolrMetricManager metricManager) {
this.metricManager = metricManager;
}
Expand All @@ -87,9 +94,13 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw
throw new SolrException(SolrException.ErrorCode.INVALID_STATE, "SolrMetricManager instance not initialized");
}

if (cc != null && AdminHandlersProxy.maybeProxyToNodes(req, rsp, cc)) {
return; // Request was proxied to other node
}

handleRequest(req.getParams(), (k, v) -> rsp.add(k, v));
}

public void handleRequest(SolrParams params, BiConsumer<String, Object> consumer) throws Exception {
boolean compact = params.getBool(COMPACT_PARAM, true);
String[] keys = params.getParams(KEY_PARAM);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@
public class SystemInfoHandler extends RequestHandlerBase
{
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String PARAM_NODE = "node";

public static String REDACT_STRING = RedactionUtils.getRedactString();

/**
* <p>
* Undocumented expert level system property to prevent doing a reverse lookup of our hostname.
* This property ill be logged as a suggested workaround if any probems are noticed when doing reverse
* This property will be logged as a suggested workaround if any problems are noticed when doing reverse
* lookup.
* </p>
*
Expand Down Expand Up @@ -130,7 +131,11 @@ private void initHostname() {
@Override
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception
{
rsp.setHttpCaching(false);
SolrCore core = req.getCore();
if (AdminHandlersProxy.maybeProxyToNodes(req, rsp, getCoreContainer(req, core))) {
return; // Request was proxied to other node
}
if (core != null) rsp.add( "core", getCoreInfo( core, req.getSchema() ) );
boolean solrCloudMode = getCoreContainer(req, core).isZooKeeperAware();
rsp.add( "mode", solrCloudMode ? "solrcloud" : "std");
Expand All @@ -142,7 +147,9 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw
rsp.add( "lucene", getLuceneInfo() );
rsp.add( "jvm", getJvmInfo() );
rsp.add( "system", getSystemInfo() );
rsp.setHttpCaching(false);
if (solrCloudMode) {
rsp.add("node", getCoreContainer(req, core).getZkController().getNodeName());
}
}

private CoreContainer getCoreContainer(SolrQueryRequest req, SolrCore core) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.solr.handler.admin;

import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.lucene.util.IOUtils;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.client.solrj.response.SimpleSolrResponse;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.util.NamedList;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class AdminHandlersProxyTest extends SolrCloudTestCase {
private CloseableHttpClient httpClient;
private CloudSolrClient solrClient;

@BeforeClass
public static void setupCluster() throws Exception {
configureCluster(2)
.addConfig("conf", configset("cloud-minimal"))
.configure();
}

@Before
@Override
public void setUp() throws Exception {
super.setUp();
solrClient = getCloudSolrClient(cluster);
solrClient.connect(1000, TimeUnit.MILLISECONDS);
httpClient = (CloseableHttpClient) solrClient.getHttpClient();
}

@After
@Override
public void tearDown() throws Exception {
super.tearDown();
IOUtils.close(solrClient, httpClient);
}

@Test
public void proxySystemInfoHandlerAllNodes() throws IOException, SolrServerException {
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", "all"));
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/system", params);
SimpleSolrResponse rsp = req.process(solrClient, null);
NamedList<Object> nl = rsp.getResponse();
assertEquals(3, nl.size());
assertTrue(nl.getName(1).endsWith("_solr"));
assertTrue(nl.getName(2).endsWith("_solr"));
assertEquals("solrcloud", ((NamedList)nl.get(nl.getName(1))).get("mode"));
assertEquals(nl.getName(2), ((NamedList)nl.get(nl.getName(2))).get("node"));
}

@Test
public void proxyMetricsHandlerAllNodes() throws IOException, SolrServerException {
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", "all"));
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/metrics", params);
SimpleSolrResponse rsp = req.process(solrClient, null);
NamedList<Object> nl = rsp.getResponse();
assertEquals(3, nl.size());
assertTrue(nl.getName(1).endsWith("_solr"));
assertTrue(nl.getName(2).endsWith("_solr"));
assertNotNull(((NamedList)nl.get(nl.getName(1))).get("metrics"));
}

@Test(expected = SolrException.class)
public void proxySystemInfoHandlerNonExistingNode() throws IOException, SolrServerException {
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", "example.com:1234_solr"));
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/system", params);
SimpleSolrResponse rsp = req.process(solrClient, null);
}

@Test
public void proxySystemInfoHandlerOneNode() {
Set<String> nodes = solrClient.getClusterStateProvider().getLiveNodes();
assertEquals(2, nodes.size());
nodes.forEach(node -> {
MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", node));
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/info/system", params);
SimpleSolrResponse rsp = null;
try {
rsp = req.process(solrClient, null);
} catch (Exception e) {
fail("Exception while proxying request to node " + node);
}
NamedList<Object> nl = rsp.getResponse();
assertEquals(2, nl.size());
assertEquals("solrcloud", ((NamedList)nl.get(nl.getName(1))).get("mode"));
assertEquals(nl.getName(1), ((NamedList)nl.get(nl.getName(1))).get("node"));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static void beforeClass() throws Exception {

@Test
public void test() throws Exception {
MetricsHandler handler = new MetricsHandler(h.getCoreContainer().getMetricManager());
MetricsHandler handler = new MetricsHandler(h.getCoreContainer());

SolrQueryResponse resp = new SolrQueryResponse();
handler.handleRequestBody(req(CommonParams.QT, "/admin/metrics", MetricsHandler.COMPACT_PARAM, "false", CommonParams.WT, "json"), resp);
Expand Down Expand Up @@ -179,7 +179,7 @@ public void test() throws Exception {

@Test
public void testCompact() throws Exception {
MetricsHandler handler = new MetricsHandler(h.getCoreContainer().getMetricManager());
MetricsHandler handler = new MetricsHandler(h.getCoreContainer());

SolrQueryResponse resp = new SolrQueryResponse();
handler.handleRequestBody(req(CommonParams.QT, "/admin/metrics", CommonParams.WT, "json", MetricsHandler.COMPACT_PARAM, "true"), resp);
Expand All @@ -197,7 +197,7 @@ public void testCompact() throws Exception {
public void testPropertyFilter() throws Exception {
assertQ(req("*:*"), "//result[@numFound='0']");

MetricsHandler handler = new MetricsHandler(h.getCoreContainer().getMetricManager());
MetricsHandler handler = new MetricsHandler(h.getCoreContainer());

SolrQueryResponse resp = new SolrQueryResponse();
handler.handleRequestBody(req(CommonParams.QT, "/admin/metrics", CommonParams.WT, "json",
Expand Down Expand Up @@ -234,7 +234,7 @@ public void testPropertyFilter() throws Exception {

@Test
public void testKeyMetrics() throws Exception {
MetricsHandler handler = new MetricsHandler(h.getCoreContainer().getMetricManager());
MetricsHandler handler = new MetricsHandler(h.getCoreContainer());

String key1 = "solr.core.collection1:CACHE.core.fieldCache";
SolrQueryResponse resp = new SolrQueryResponse();
Expand Down

0 comments on commit 17a02c1

Please sign in to comment.