You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ja...@apache.org on 2018/08/08 11:40:27 UTC

lucene-solr:branch_7x: SOLR-7767: "ZK Status" sub menu under "Cloud" tab to see status of zookeeper ensemble

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_7x b7f14648f -> 572557d8f


SOLR-7767: "ZK Status" sub menu under "Cloud" tab to see status of zookeeper ensemble

(cherry picked from commit 9306922)


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/572557d8
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/572557d8
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/572557d8

Branch: refs/heads/branch_7x
Commit: 572557d8fd2a6857e0fc9ab47de40601182c0915
Parents: b7f1464
Author: Jan Høydahl <ja...@apache.org>
Authored: Wed Aug 8 12:43:19 2018 +0200
Committer: Jan Høydahl <ja...@apache.org>
Committed: Wed Aug 8 13:14:37 2018 +0200

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   2 +
 .../org/apache/solr/core/CoreContainer.java     |   3 +
 .../handler/admin/ZookeeperStatusHandler.java   | 222 +++++++++++++++++++
 .../admin/ZookeeperStatusHandlerTest.java       |  85 +++++++
 solr/solr-ref-guide/src/cloud-screens.adoc      |   5 +
 .../src/images/cloud-screens/cloud-zkstatus.png | Bin 0 -> 175359 bytes
 .../apache/solr/common/params/CommonParams.java |   1 +
 solr/webapp/web/css/angular/cloud.css           |  71 +++++-
 solr/webapp/web/css/angular/menu.css            |   1 +
 solr/webapp/web/index.html                      |   1 +
 solr/webapp/web/js/angular/controllers/cloud.js |  38 +++-
 solr/webapp/web/js/angular/services.js          |   6 +
 solr/webapp/web/partials/cloud.html             |  59 +++++
 13 files changed, 491 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 5578ac3..68c8155 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -97,6 +97,8 @@ New Features
 
 * SOLR-8207: Add "Nodes" view to the Admin UI "Cloud" tab, listing nodes and key metrics (janhoy)
 
+* SOLR-7767: "ZK Status" sub menu under "Cloud" tab to see status of zookeeper ensemble (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

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/core/src/java/org/apache/solr/core/CoreContainer.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 67b8794..cf51268 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -84,6 +84,7 @@ import org.apache.solr.handler.admin.SecurityConfHandler;
 import org.apache.solr.handler.admin.SecurityConfHandlerLocal;
 import org.apache.solr.handler.admin.SecurityConfHandlerZk;
 import org.apache.solr.handler.admin.ZookeeperInfoHandler;
+import org.apache.solr.handler.admin.ZookeeperStatusHandler;
 import org.apache.solr.handler.component.ShardHandlerFactory;
 import org.apache.solr.logging.LogWatcher;
 import org.apache.solr.logging.MDCLoggingContext;
@@ -118,6 +119,7 @@ import static org.apache.solr.common.params.CommonParams.INFO_HANDLER_PATH;
 import static org.apache.solr.common.params.CommonParams.METRICS_HISTORY_PATH;
 import static org.apache.solr.common.params.CommonParams.METRICS_PATH;
 import static org.apache.solr.common.params.CommonParams.ZK_PATH;
+import static org.apache.solr.common.params.CommonParams.ZK_STATUS_PATH;
 import static org.apache.solr.core.CorePropertiesLocator.PROPERTIES_FILENAME;
 import static org.apache.solr.security.AuthenticationPlugin.AUTHENTICATION_PLUGIN_PROP;
 
@@ -561,6 +563,7 @@ public class CoreContainer {
     this.backupRepoFactory = new BackupRepositoryFactory(cfg.getBackupRepositoryPlugins());
 
     createHandler(ZK_PATH, ZookeeperInfoHandler.class.getName(), ZookeeperInfoHandler.class);
+    createHandler(ZK_STATUS_PATH, ZookeeperStatusHandler.class.getName(), ZookeeperStatusHandler.class);
     collectionsHandler = createHandler(COLLECTIONS_HANDLER_PATH, cfg.getCollectionsHandlerClass(), CollectionsHandler.class);
     infoHandler        = createHandler(INFO_HANDLER_PATH, cfg.getInfoHandlerClass(), InfoHandler.class);
     coreAdminHandler   = createHandler(CORES_HANDLER_PATH, cfg.getCoreAdminHandlerClass(), CoreAdminHandler.class);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperStatusHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperStatusHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperStatusHandler.java
new file mode 100644
index 0000000..8842437
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperStatusHandler.java
@@ -0,0 +1,222 @@
+/*
+ * 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.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.lang.invoke.MethodHandles;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Zookeeper Status handler, talks to ZK using sockets and four-letter words
+ *
+ * @since solr 7.5
+ */
+public final class ZookeeperStatusHandler extends RequestHandlerBase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final int ZOOKEEPER_DEFAULT_PORT = 2181;
+  private static final String STATUS_RED = "red";
+  private static final String STATUS_GREEN = "green";
+  private static final String STATUS_YELLOW = "yellow";
+  private static final String STATUS_NA = "N/A";
+  private CoreContainer cores;
+
+  public ZookeeperStatusHandler(CoreContainer cc) {
+    this.cores = cc;
+  }
+  
+  @Override
+  public String getDescription() {
+    return "Fetch Zookeeper status";
+  }
+
+  @Override
+  public Category getCategory() {
+    return Category.ADMIN;
+  }
+
+  @Override
+  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+    final SolrParams params = req.getParams();
+    Map<String, String> map = new HashMap<>(1);
+    NamedList values = rsp.getValues();
+    values.add("zkStatus", getZkStatus(cores.getZkController().getZkServerAddress()));
+  }
+
+  /*
+   Gets all info from ZK API and returns as a map
+   */
+  protected Map<String, Object> getZkStatus(String zkHost) {
+    Map<String, Object> zkStatus = new HashMap<>();
+    List<String> zookeepers = Arrays.asList(zkHost.split("/")[0].split(","));
+    List<Object> details = new ArrayList<>();
+    int numOk = 0;
+    String status = STATUS_NA;
+    int standalone = 0;
+    int followers = 0;
+    int reportedFollowers = 0;
+    int leaders = 0;
+    List<String> errors = new ArrayList<>();
+    for (String zk : zookeepers) {
+      try {
+        Map<String, Object> stat = monitorZookeeper(zk);
+        details.add(stat);
+        if ("true".equals(String.valueOf(stat.get("ok")))) {
+          numOk++;
+        }
+        String state = String.valueOf(stat.get("zk_server_state"));
+        if ("follower".equals(state)) {
+          followers++;
+        } else if ("leader".equals(state)) {
+          leaders++;
+          reportedFollowers = Integer.parseInt(String.valueOf(stat.get("zk_followers")));
+        } else if ("standalone".equals(state)) {
+          standalone++;
+        }
+      } catch (SolrException se) {
+        log.warn("Failed talking to zookeeper" + zk, se);
+        errors.add(se.getMessage());
+        Map<String, Object> stat = new HashMap<>();
+        stat.put("host", zk);
+        stat.put("ok", false);
+        details.add(stat);
+      }       
+    }
+    zkStatus.put("ensembleSize", zookeepers.size());
+    zkStatus.put("zkHost", zkHost);
+    zkStatus.put("details", details);
+    if (followers+leaders > 0 && standalone > 0) {
+      status = STATUS_RED;
+      errors.add("The zk nodes do not agree on their mode, check details");
+    }
+    if (standalone > 1) {
+      status = STATUS_RED;
+      errors.add("Only one zk allowed in standalone mode");
+    }
+    if (leaders > 1) {
+      zkStatus.put("mode", "ensemble");
+      status = STATUS_RED;
+      errors.add("Only one leader allowed, got " + leaders);
+    }
+    if (followers > 0 && leaders == 0) {
+      zkStatus.put("mode", "ensemble");
+      status = STATUS_RED;
+      errors.add("We do not have a leader");
+    }
+    if (leaders > 0 && followers != reportedFollowers) {
+      zkStatus.put("mode", "ensemble");
+      status = STATUS_RED;
+      errors.add("Leader reports " + reportedFollowers + " followers, but we only found " + followers + 
+        ". Please check zkHost configuration");
+    }
+    if (followers+leaders == 0 && standalone == 1) {
+      zkStatus.put("mode", "standalone");
+    }
+    if (followers+leaders > 0 && (zookeepers.size())%2 == 0) {
+      if (!STATUS_RED.equals(status)) {
+        status = STATUS_YELLOW;
+      }
+      errors.add("We have an even number of zookeepers which is not recommended");
+    }
+    if (followers+leaders > 0 && standalone == 0) {
+      zkStatus.put("mode", "ensemble");
+    }
+    if (status.equals(STATUS_NA)) {
+      if (numOk == zookeepers.size()) {
+        status = STATUS_GREEN;
+      } else if (numOk < zookeepers.size() && numOk > zookeepers.size() / 2) {
+        status = STATUS_YELLOW;
+        errors.add("Some zookeepers are down: " + numOk + "/" + zookeepers.size());
+      } else {
+        status = STATUS_RED;
+        errors.add("Mismatch in number of zookeeper nodes live. numOK=" + numOk + ", expected " + zookeepers.size());
+      }
+    }
+    zkStatus.put("status", status);
+    if (!errors.isEmpty()) {
+      zkStatus.put("errors", errors);
+    }
+    return zkStatus;
+  }
+
+  private Map<String, Object> monitorZookeeper(String zkHostPort) {
+    List<String> lines = getZkRawResponse(zkHostPort, "mntr");
+    Map<String, Object> obj = new HashMap<>();
+    obj.put("host", zkHostPort);
+    obj.put("ok", "imok".equals(getZkRawResponse(zkHostPort, "ruok").get(0)));
+    for (String line : lines) {
+      obj.put(line.split("\t")[0], line.split("\t")[1]);
+    }
+    lines = getZkRawResponse(zkHostPort, "conf");
+    for (String line : lines) {
+      obj.put(line.split("=")[0], line.split("=")[1]);
+    }
+    return obj;
+  }
+  
+  /**
+   * Sends a four-letter-word command to one particular Zookeeper server and returns the response as list of strings
+   * @param zkHostPort the host:port for one zookeeper server to access
+   * @param fourLetterWordCommand the custom 4-letter command to send to Zookeeper
+   * @return a list of lines returned from Zookeeper
+   */
+  private List<String> getZkRawResponse(String zkHostPort, String fourLetterWordCommand) {
+    String[] hostPort = zkHostPort.split(":");
+    String host = hostPort[0];
+    int port = ZOOKEEPER_DEFAULT_PORT;
+    if (hostPort.length > 1) {
+      port = Integer.parseInt(hostPort[1]);
+    }
+    try (
+        Socket socket = new Socket(host, port);
+        Writer writer = new OutputStreamWriter(socket.getOutputStream(), "utf-8");
+        PrintWriter out = new PrintWriter(writer, true);
+        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));) {
+      out.println(fourLetterWordCommand);
+      List<String> response = in.lines().collect(Collectors.toList());
+      log.debug("Got response from ZK on host {} and port {}: {}", host, port, response);
+      if (response == null || response.isEmpty()) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Empty response from Zookeeper " + zkHostPort);
+      }
+      return response;
+    } catch (IOException e) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed talking to Zookeeper " + zkHostPort, e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperStatusHandlerTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperStatusHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperStatusHandlerTest.java
new file mode 100644
index 0000000..7ec8bf2
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperStatusHandlerTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+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.client.solrj.response.DelegationTokenResponse;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.params.ModifiableSolrParams;
+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 ZookeeperStatusHandlerTest extends SolrCloudTestCase {
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1)
+        .addConfig("conf", configset("cloud-minimal"))
+        .configure();
+  }
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+  }
+
+  @After
+  @Override
+  public void tearDown() throws Exception {
+    super.tearDown();
+  }
+
+  /*
+    Test the monitoring endpoint, used in the Cloud => ZkStatus Admin UI screen
+    NOTE: We do not currently test with multiple zookeepers, but the only difference is that there are multiple "details" objects and mode is "ensemble"... 
+   */
+  @Test
+  public void monitorZookeeper() throws IOException, SolrServerException, InterruptedException, ExecutionException, TimeoutException {
+    URL baseUrl = cluster.getJettySolrRunner(0).getBaseUrl();
+    HttpSolrClient solr = new HttpSolrClient.Builder(baseUrl.toString()).build();
+    GenericSolrRequest mntrReq = new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper/status", new ModifiableSolrParams());
+    mntrReq.setResponseParser(new DelegationTokenResponse.JsonMapResponseParser());
+    NamedList<Object> nl = solr.httpUriRequest(mntrReq).future.get(1000, TimeUnit.MILLISECONDS);
+
+    assertEquals("zkStatus", nl.getName(1));
+    Map<String,Object> zkStatus = (Map<String,Object>) nl.get("zkStatus");
+    assertEquals("green", zkStatus.get("status"));
+    assertEquals("standalone", zkStatus.get("mode"));
+    assertEquals(1L, zkStatus.get("ensembleSize"));
+    List<Object> detailsList = (List<Object>)zkStatus.get("details");
+    assertEquals(1, detailsList.size());
+    Map<String,Object> details = (Map<String,Object>) detailsList.get(0);
+    assertEquals(true, details.get("ok"));
+    assertTrue(Integer.parseInt((String) details.get("zk_znode_count")) > 50);
+    solr.close();
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/solr-ref-guide/src/cloud-screens.adoc
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/cloud-screens.adoc b/solr/solr-ref-guide/src/cloud-screens.adoc
index 34982ee..cc5a5eb 100644
--- a/solr/solr-ref-guide/src/cloud-screens.adoc
+++ b/solr/solr-ref-guide/src/cloud-screens.adoc
@@ -42,6 +42,11 @@ image::images/cloud-screens/cloud-tree.png[image,width=487,height=250]
 
 As an aid to debugging, the data shown in the "Tree" view can be exported locally using the following command `bin/solr zk ls -r /`
 
+== ZK Status view
+The "ZK Status" view gives an overview over the Zookeepers used by Solr. It lists whether running in `standalone` or `ensemble` mode, shows how many zookeepers are configured, and then displays a table listing detailed monitoring status for each of the zookeepers, inlcuding who is the leader, configuration parameters and more.
+
+image::images/cloud-screens/cloud-zkstatus.png[image,width=512,height=509]
+
 == Graph views
 The "Graph" view shows a graph of each collection, the shards that make up those collections, and the addresses and type ("NRT", "TLOG" or "PULL") of each replica for each shard.
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/solr-ref-guide/src/images/cloud-screens/cloud-zkstatus.png
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/images/cloud-screens/cloud-zkstatus.png b/solr/solr-ref-guide/src/images/cloud-screens/cloud-zkstatus.png
new file mode 100644
index 0000000..b1b7452
Binary files /dev/null and b/solr/solr-ref-guide/src/images/cloud-screens/cloud-zkstatus.png differ

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java
index 600e479..28865d2 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java
@@ -181,6 +181,7 @@ public interface CommonParams {
   String AUTHZ_PATH = "/admin/authorization";
   String AUTHC_PATH = "/admin/authentication";
   String ZK_PATH = "/admin/zookeeper";
+  String ZK_STATUS_PATH = "/admin/zookeeper/status";
   String SYSTEM_INFO_PATH = "/admin/info/system";
   String METRICS_PATH = "/admin/metrics";
   String METRICS_HISTORY_PATH = "/admin/metrics/history";

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/webapp/web/css/angular/cloud.css
----------------------------------------------------------------------
diff --git a/solr/webapp/web/css/angular/cloud.css b/solr/webapp/web/css/angular/cloud.css
index c3d54a5..5c8ce45 100644
--- a/solr/webapp/web/css/angular/cloud.css
+++ b/solr/webapp/web/css/angular/cloud.css
@@ -536,7 +536,8 @@ limitations under the License.
 }
 
 /* Styling of reload and details buttons */
-#content #cloud #controls
+#content #cloud #controls,
+#content #cloud #frame #zk-status-content #zk-controls
 {
   display: block;
   height: 30px;
@@ -641,4 +642,70 @@ limitations under the License.
     border-radius: 4px;
     background-color: rgba(0,0,0,.5);
     -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5);
-}
\ No newline at end of file
+}
+#content #cloud #zk-table td,
+#content #cloud #zk-table th 
+{
+  border: 0px solid #ddd;
+  border-bottom: 0.50px solid #eee;
+  padding-right: 5px;
+  padding-left: 5px;
+}
+
+#content #cloud #zk-table th 
+{
+  border-bottom: 1px solid #ddd;
+  border-top: 1px solid #ddd;
+  font-weight: bolder;
+  font-stretch: extra-expanded;
+  background: #F8F8F8;
+}
+
+#content #cloud #zk-table
+{
+  border-top: 1px solid #c0c0c0;
+  margin-top: 10px;
+  border-collapse: collapse;
+
+  font-weight: bold;
+}
+
+#content #cloud #zk-table #detail-divider 
+{
+  background-color: #f8f8f8;
+  height: 10px;
+}
+
+.zookeeper-status
+{
+  font-size: large;
+}
+
+.zookeeper-errors
+{
+  background-color: lightpink;
+  padding: 10px;
+  border: 1px;
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+.zookeeper-errors li::before 
+{
+  content: "- ";
+}
+
+.zkstatus-green
+{
+  color: darkgreen;
+}
+
+.zkstatus-yellow
+{
+  color: orange;
+}
+
+.zkstatus-red
+{
+  color: red;
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/webapp/web/css/angular/menu.css
----------------------------------------------------------------------
diff --git a/solr/webapp/web/css/angular/menu.css b/solr/webapp/web/css/angular/menu.css
index ba5e0b6..f4e04c1 100644
--- a/solr/webapp/web/css/angular/menu.css
+++ b/solr/webapp/web/css/angular/menu.css
@@ -261,6 +261,7 @@ limitations under the License.
 #menu #cloud.global p a { background-image: url( ../../img/ico/network-cloud.png ); }
 #menu #cloud.global .tree a { background-image: url( ../../img/ico/folder-tree.png ); }
 #menu #cloud.global .nodes a { background-image: url( ../../img/solr-ico.png ); }
+#menu #cloud.global .zkstatus a { background-image: url( ../../img/ico/node-master.png ); }
 #menu #cloud.global .graph a { background-image: url( ../../img/ico/molecule.png ); }
 #menu #cloud.global .rgraph a { background-image: url( ../../img/ico/asterisk.png ); }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/webapp/web/index.html
----------------------------------------------------------------------
diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html
index 256af89..0663805 100644
--- a/solr/webapp/web/index.html
+++ b/solr/webapp/web/index.html
@@ -152,6 +152,7 @@ limitations under the License.
               <ul ng-show="showingCloud">
                 <li class="nodes" ng-class="{active:page=='cloud-nodes'}"><a href="#/~cloud?view=nodes">Nodes</a></li>
                 <li class="tree" ng-class="{active:page=='cloud-tree'}"><a href="#/~cloud?view=tree">Tree</a></li>
+                <li class="zkstatus" ng-class="{active:page=='cloud-zkstatus'}"><a href="#/~cloud?view=zkstatus">ZK Status</a></li>
                 <li class="graph" ng-class="{active:page=='cloud-graph'}"><a href="#/~cloud?view=graph">Graph</a></li>
                 <li class="rgraph" ng-class="{active:page=='cloud-rgraph'}"><a href="#/~cloud?view=rgraph">Graph (Radial)</a></li>
               </ul>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/webapp/web/js/angular/controllers/cloud.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js b/solr/webapp/web/js/angular/controllers/cloud.js
index 08eea38..abe0ef6 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -16,7 +16,7 @@
 */
 
 solrAdminApp.controller('CloudController',
-    function($scope, $location, Zookeeper, Constants, Collections, System, Metrics) {
+    function($scope, $location, Zookeeper, Constants, Collections, System, Metrics, ZookeeperStatus) {
 
         $scope.showDebug = false;
 
@@ -41,6 +41,9 @@ solrAdminApp.controller('CloudController',
         } else if (view === "nodes") {
             $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE);
             nodesSubController($scope, Collections, System, Metrics);
+        } else if (view === "zkstatus") {
+            $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE);
+            zkStatusSubController($scope, ZookeeperStatus, false);
         }
     }
 );
@@ -486,7 +489,39 @@ var nodesSubController = function($scope, Collections, System, Metrics) {
   $scope.initClusterState();
 };
 
+var zkStatusSubController = function($scope, ZookeeperStatus) {
+    $scope.showZkStatus = true;
+    $scope.showNodes = false;
+    $scope.showTree = false;
+    $scope.showGraph = false;
+    $scope.tree = {};
+    $scope.showData = false;
+    $scope.showDetails = false;
+    
+    $scope.toggleDetails = function() {
+      $scope.showDetails = !$scope.showDetails === true;
+    };
+
+    $scope.initZookeeper = function() {
+      ZookeeperStatus.monitor({}, function(data) {
+        $scope.zkState = data.zkStatus;
+        $scope.mainKeys = ["ok", "clientPort", "zk_server_state", "zk_version", 
+          "zk_approximate_data_size", "zk_znode_count", "zk_num_alive_connections"];
+        $scope.detailKeys = ["dataDir", "dataLogDir", 
+          "zk_avg_latency", "zk_max_file_descriptor_count", "zk_watch_count", 
+          "zk_packets_sent", "zk_packets_received",
+          "tickTime", "maxClientCnxns", "minSessionTimeout", "maxSessionTimeout"];
+        $scope.ensembleMainKeys = ["serverId", "electionPort", "quorumPort"];
+        $scope.ensembleDetailKeys = ["peerType", "electionAlg", "initLimit", "syncLimit",
+          "zk_followers", "zk_synced_followers", "zk_pending_syncs"];
+      });
+    };
+
+    $scope.initZookeeper();
+};
+
 var treeSubController = function($scope, Zookeeper) {
+    $scope.showZkStatus = false;
     $scope.showTree = true;
     $scope.showGraph = false;
     $scope.tree = {};
@@ -545,6 +580,7 @@ function secondsForHumans ( seconds ) {
 }
 
 var graphSubController = function ($scope, Zookeeper, isRadial) {
+    $scope.showZkStatus = false;
     $scope.showTree = false;
     $scope.showGraph = true;
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/webapp/web/js/angular/services.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index 66f2654..e3dcd3f 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -81,6 +81,12 @@ solrAdminServices.factory('System',
       }}
     });
   }])
+.factory('ZookeeperStatus',
+  ['$resource', function($resource) {
+    return $resource('admin/zookeeper/status', {wt:'json', _:Date.now()}, {
+      "monitor": {}
+    });
+  }])
 .factory('Properties',
   ['$resource', function($resource) {
     return $resource('admin/info/properties', {'wt':'json', '_':Date.now()});

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/572557d8/solr/webapp/web/partials/cloud.html
----------------------------------------------------------------------
diff --git a/solr/webapp/web/partials/cloud.html b/solr/webapp/web/partials/cloud.html
index 078c9af..825e195 100644
--- a/solr/webapp/web/partials/cloud.html
+++ b/solr/webapp/web/partials/cloud.html
@@ -18,6 +18,65 @@ limitations under the License.
 
   <div id="frame">
 
+    <div id="zk-status-content" class="content clearfix" ng-show="showZkStatus">
+      <div id="zk-controls">
+        <a class="reload" ng-click="initZookeeper()"><span>Refresh</span></a>
+        <a class="details-button" ng-click="toggleDetails()" ng-class="{on:showDetails}">
+          <span>Toggle details</span>
+        </a>
+      </div>
+
+      <div class="zookeeper-status">Status: <span class="zkstatus-{{zkState.status}}">{{zkState.status}}</span></div>
+      <div class="zookeeper-errors" ng-show="zkState.errors">
+        Errors:
+        <ul>
+          <li ng-repeat="error in zkState.errors">{{error}}</li>
+        </ul>
+      </div>
+      <div>ZK connection string: {{zkState.zkHost}}</div>
+      <div>Ensemble size: {{zkState.ensembleSize}}</div>
+      <div>Ensemble mode: {{zkState.mode}}</div>
+      
+      <table id="zk-table">
+        <thead>
+          <tr>
+            <th></th>
+            <th ng-repeat="host in zkState.details">{{host.host}}</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr ng-repeat="key in mainKeys">
+            <td>{{key}}</td>
+            <td ng-repeat="host in zkState.details" ng-style="key === 'zk_server_state' && host[key] === 'leader' ? {'font-weight': 'bold'} : {'font-weight': 'normal'}">
+              {{key === 'zk_version' ? host[key].split("-")[0] : host[key]}}
+            </td>
+          </tr>
+          <tr ng-repeat="key in ensembleMainKeys" ng-show="zkState.mode === 'ensemble'">
+            <td>{{key}}</td>
+            <td ng-repeat="host in zkState.details">
+              {{host[key]}}
+            </td>
+          </tr>
+          <tr id="detail-divider" ng-show="showDetails" >
+            <td ng-class="details"></td>
+            <td ng-repeat="host in zkState.details" ng-class="details"></td>
+          </tr>
+          <tr ng-repeat="key in detailKeys" ng-show="showDetails">
+            <td>{{key}}</td>
+            <td ng-repeat="host in zkState.details">
+              {{host[key]}}
+            </td>
+          </tr>
+          <tr ng-repeat="key in ensembleDetailKeys" ng-show="showDetails && zkState.mode === 'ensemble'">
+            <td>{{key}}</td>
+            <td ng-repeat="host in zkState.details">
+              {{host[key]}}
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
     <div id="tree-content" class="content clearfix" ng-show="showTree">
     <jstree class="tree" on-select="showTreeLink(url)" id="tree" data="tree"></jstree>