You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by nd...@apache.org on 2020/04/14 23:09:59 UTC

[hbase] branch branch-2 updated: HBASE-23994: Add WebUI to Canary (#1292)

This is an automated email from the ASF dual-hosted git repository.

ndimiduk pushed a commit to branch branch-2
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2 by this push:
     new f61aa02  HBASE-23994: Add WebUI to Canary (#1292)
f61aa02 is described below

commit f61aa0292d5fad2ace853becae62524eee5642d7
Author: GeorryHuang <21...@qq.com>
AuthorDate: Wed Apr 15 06:52:22 2020 +0800

    HBASE-23994: Add WebUI to Canary (#1292)
    
    Signed-off-by: Duo Zhang <zh...@apache.org>
    Signed-off-by: Nick Dimiduk <nd...@apache.org>
---
 .../hadoop/hbase/tmpl/tool/CanaryStatusTmpl.jamon  | 156 +++++++++++++++++++++
 .../hadoop/hbase/tool/CanaryStatusServlet.java     |  49 +++++++
 .../org/apache/hadoop/hbase/tool/CanaryTool.java   |  78 ++++++++++-
 .../main/resources/hbase-webapps/canary/canary.jsp |  20 +++
 .../main/resources/hbase-webapps/canary/index.html |  20 +++
 .../hadoop/hbase/tool/TestCanaryStatusServlet.java | 116 +++++++++++++++
 6 files changed, 437 insertions(+), 2 deletions(-)

diff --git a/hbase-server/src/main/jamon/org/apache/hadoop/hbase/tmpl/tool/CanaryStatusTmpl.jamon b/hbase-server/src/main/jamon/org/apache/hadoop/hbase/tmpl/tool/CanaryStatusTmpl.jamon
new file mode 100644
index 0000000..e2d29ee
--- /dev/null
+++ b/hbase-server/src/main/jamon/org/apache/hadoop/hbase/tmpl/tool/CanaryStatusTmpl.jamon
@@ -0,0 +1,156 @@
+
+<%doc>
+
+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.
+</%doc>
+<%args>
+RegionStdOutSink sink;
+</%args>
+<%import>
+java.util.Map;
+java.util.concurrent.atomic.LongAdder;
+org.apache.hadoop.hbase.ServerName;
+org.apache.hadoop.hbase.tool.CanaryTool.RegionStdOutSink;
+</%import>
+
+<!--[if IE]>
+<!DOCTYPE html>
+<![endif]-->
+<?xml version="1.0" encoding="UTF-8" ?>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Canary</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <link href="/static/css/bootstrap.min.css" rel="stylesheet">
+    <link href="/static/css/bootstrap-theme.min.css" rel="stylesheet">
+    <link href="/static/css/hbase.css" rel="stylesheet">
+  </head>
+
+  <body>
+
+    <div class="navbar  navbar-fixed-top navbar-default">
+        <div class="container">
+            <div class="navbar-header">
+                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+                    <span class="icon-bar"></span>
+                    <span class="icon-bar"></span>
+                    <span class="icon-bar"></span>
+                </button>
+                <a class="navbar-brand" href="/canary-status"><img src="/static/hbase_logo_small.png" alt="HBase Logo"/></a>
+            </div>
+        </div>
+    </div>
+
+  <div class="container">
+    <section>
+    <h2>Failed Servers</h2>
+        <%java>
+           Map<ServerName, LongAdder> perServerFailuresCount = sink.getPerServerFailuresCount();
+        </%java>
+        <table class="table table-striped">
+          <tr>
+              <th>Server</th>
+              <th>Failures Count</th>
+          </tr>
+          <%if (perServerFailuresCount != null && perServerFailuresCount.size() > 0)%>
+          <%for Map.Entry<ServerName, LongAdder> entry : perServerFailuresCount.entrySet() %>
+          <tr>
+              <td><& serverNameLink ; serverName = entry.getKey() &></td>
+              <td><% entry.getValue() %></td>
+          </tr>
+          </%for>
+          </%if>
+          <tr><td>Total Failed Servers: <% (perServerFailuresCount != null) ? perServerFailuresCount.size() : 0 %></td></tr>
+        </table>
+    </section>
+    <section>
+      <h2>Failed Tables</h2>
+            <%java>
+               Map<String, LongAdder> perTableFailuresCount = sink.getPerTableFailuresCount();
+            </%java>
+            <table class="table table-striped">
+              <tr>
+                  <th>Table</th>
+                  <th>Failures Count</th>
+              </tr>
+              <%if (perTableFailuresCount != null && perTableFailuresCount.size() > 0)%>
+              <%for Map.Entry<String, LongAdder> entry : perTableFailuresCount.entrySet()%>
+              <tr>
+                  <td><% entry.getKey() %></td>
+                  <td><% entry.getValue() %></td>
+              </tr>
+              </%for>
+              </%if>
+              <tr><td>Total Failed Tables: <% (perTableFailuresCount != null) ? perTableFailuresCount.size() : 0 %></td></tr>
+            </table>
+    </section>
+
+        <section>
+            <h2>Software Attributes</h2>
+            <table id="attributes_table" class="table table-striped">
+                <tr>
+                    <th>Attribute Name</th>
+                    <th>Value</th>
+                    <th>Description</th>
+                </tr>
+                <tr>
+                    <td>HBase Version</td>
+                    <td><% org.apache.hadoop.hbase.util.VersionInfo.getVersion() %>, r<% org.apache.hadoop.hbase.util.VersionInfo.getRevision() %></td><td>HBase version and revision</td>
+                </tr>
+                <tr>
+                    <td>HBase Compiled</td>
+                    <td><% org.apache.hadoop.hbase.util.VersionInfo.getDate() %>, <% org.apache.hadoop.hbase.util.VersionInfo.getUser() %></td>
+                    <td>When HBase version was compiled and by whom</td>
+                </tr>
+                <tr>
+                    <td>Hadoop Version</td>
+                    <td><% org.apache.hadoop.util.VersionInfo.getVersion() %>, r<% org.apache.hadoop.util.VersionInfo.getRevision() %></td>
+                    <td>Hadoop version and revision</td>
+                </tr>
+                <tr>
+                    <td>Hadoop Compiled</td>
+                    <td><% org.apache.hadoop.util.VersionInfo.getDate() %>, <% org.apache.hadoop.util.VersionInfo.getUser() %></td>
+                    <td>When Hadoop version was compiled and by whom</td>
+                </tr>
+            </table>
+        </section>
+        </div>
+    </div> <!-- /container -->
+
+    <script src="/static/js/jquery.min.js" type="text/javascript"></script>
+    <script src="/static/js/bootstrap.min.js" type="text/javascript"></script>
+    <script src="/static/js/tab.js" type="text/javascript"></script>
+  </body>
+</html>
+
+<%def serverNameLink>
+        <%args>
+        ServerName serverName;
+        </%args>
+        <%java>
+        int infoPort = serverName.getPort() + 1;
+        String url = "//" + serverName.getHostname() + ":" + infoPort + "/";
+        </%java>
+
+        <%if (infoPort > 0) %>
+            <a href="<% url %>"><% serverName.getServerName() %></a>
+        <%else>
+            <% serverName.getServerName() %>
+        </%if>
+</%def>
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/tool/CanaryStatusServlet.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/tool/CanaryStatusServlet.java
new file mode 100644
index 0000000..ce214a7
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/tool/CanaryStatusServlet.java
@@ -0,0 +1,49 @@
+/**
+ *
+ * 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.hadoop.hbase.tool;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.hadoop.hbase.tmpl.tool.CanaryStatusTmpl;
+import org.apache.yetus.audience.InterfaceAudience;
+
+
+@InterfaceAudience.Private
+public class CanaryStatusServlet extends HttpServlet {
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+    throws ServletException, IOException {
+    CanaryTool.RegionStdOutSink sink =
+      (CanaryTool.RegionStdOutSink) getServletContext().getAttribute(
+        "sink");
+    if (sink == null) {
+      throw new ServletException(
+        "RegionStdOutSink is null! The CanaryTool's InfoServer is not initialized correctly");
+    }
+
+    resp.setContentType("text/html");
+
+    CanaryStatusTmpl tmpl = new CanaryStatusTmpl();
+    tmpl.render(resp.getWriter(), sink);
+  }
+
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/tool/CanaryTool.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/tool/CanaryTool.java
index af9b879..8438840 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/tool/CanaryTool.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/tool/CanaryTool.java
@@ -24,6 +24,7 @@ import static org.apache.hadoop.hbase.HConstants.ZOOKEEPER_ZNODE_PARENT;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.net.BindException;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -39,6 +40,7 @@ import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
@@ -81,6 +83,7 @@ import org.apache.hadoop.hbase.client.Scan;
 import org.apache.hadoop.hbase.client.Table;
 import org.apache.hadoop.hbase.client.TableDescriptor;
 import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter;
+import org.apache.hadoop.hbase.http.InfoServer;
 import org.apache.hadoop.hbase.tool.CanaryTool.RegionTask.TaskType;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
@@ -122,6 +125,37 @@ import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
  */
 @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
 public class CanaryTool implements Tool, Canary {
+  public static final String HBASE_CANARY_INFO_PORT = "hbase.canary.info.port";
+
+  public static final int DEFAULT_CANARY_INFOPORT = 16050;
+
+  public static final String HBASE_CANARY_INFO_BINDADDRESS = "hbase.canary.info.bindAddress";
+
+  private InfoServer infoServer;
+
+  private void putUpWebUI() throws IOException {
+    int port = conf.getInt(HBASE_CANARY_INFO_PORT, DEFAULT_CANARY_INFOPORT);
+    // -1 is for disabling info server
+    if (port < 0) {
+      return;
+    }
+    if (zookeeperMode) {
+      LOG.info("WebUI is not supported in Zookeeper mode");
+    } else if (regionServerMode) {
+      LOG.info("WebUI is not supported in RegionServer mode");
+    } else {
+      String addr = conf.get(HBASE_CANARY_INFO_BINDADDRESS, "0.0.0.0");
+      try {
+        infoServer = new InfoServer("canary", addr, port, false, conf);
+        infoServer.addUnprivilegedServlet("canary", "/canary-status", CanaryStatusServlet.class);
+        infoServer.setAttribute("sink", this.sink);
+        infoServer.start();
+        LOG.info("Bind Canary http info server to {}:{} ", addr, port);
+      } catch (BindException e) {
+        LOG.warn("Failed binding Canary http info server to {}:{}", addr, port, e);
+      }
+    }
+  }
 
   @Override
   public int checkRegions(String[] targets) throws Exception {
@@ -273,10 +307,45 @@ public class CanaryTool implements Tool, Canary {
   public static class RegionStdOutSink extends StdOutSink {
     private Map<String, LongAdder> perTableReadLatency = new HashMap<>();
     private LongAdder writeLatency = new LongAdder();
-    private final Map<String, List<RegionTaskResult>> regionMap = new ConcurrentHashMap<>();
+    private final ConcurrentMap<String, List<RegionTaskResult>> regionMap =
+      new ConcurrentHashMap<>();
+    private ConcurrentMap<ServerName, LongAdder> perServerFailuresCount =
+      new ConcurrentHashMap<>();
+    private ConcurrentMap<String, LongAdder> perTableFailuresCount = new ConcurrentHashMap<>();
+
+    public ConcurrentMap<ServerName, LongAdder> getPerServerFailuresCount() {
+      return perServerFailuresCount;
+    }
+
+    public ConcurrentMap<String, LongAdder> getPerTableFailuresCount() {
+      return perTableFailuresCount;
+    }
+
+    public void resetFailuresCountDetails() {
+      perServerFailuresCount.clear();
+      perTableFailuresCount.clear();
+    }
+
+    private void incFailuresCountDetails(ServerName serverName, RegionInfo region) {
+      perServerFailuresCount.compute(serverName, (server, count) -> {
+        if (count == null) {
+          count = new LongAdder();
+        }
+        count.increment();
+        return count;
+      });
+      perTableFailuresCount.compute(region.getTable().getNameAsString(), (tableName, count) -> {
+        if (count == null) {
+          count = new LongAdder();
+        }
+        count.increment();
+        return count;
+      });
+    }
 
     public void publishReadFailure(ServerName serverName, RegionInfo region, Exception e) {
       incReadFailureCount();
+      incFailuresCountDetails(serverName, region);
       LOG.error("Read from {} on serverName={} failed",
           region.getRegionNameAsString(), serverName, e);
     }
@@ -284,6 +353,7 @@ public class CanaryTool implements Tool, Canary {
     public void publishReadFailure(ServerName serverName, RegionInfo region,
         ColumnFamilyDescriptor column, Exception e) {
       incReadFailureCount();
+      incFailuresCountDetails(serverName, region);
       LOG.error("Read from {} on serverName={}, columnFamily={} failed",
           region.getRegionNameAsString(), serverName,
           column.getNameAsString(), e);
@@ -304,12 +374,14 @@ public class CanaryTool implements Tool, Canary {
 
     public void publishWriteFailure(ServerName serverName, RegionInfo region, Exception e) {
       incWriteFailureCount();
+      incFailuresCountDetails(serverName, region);
       LOG.error("Write to {} on {} failed", region.getRegionNameAsString(), serverName, e);
     }
 
     public void publishWriteFailure(ServerName serverName, RegionInfo region,
         ColumnFamilyDescriptor column, Exception e) {
       incWriteFailureCount();
+      incFailuresCountDetails(serverName, region);
       LOG.error("Write to {} on {} {} failed", region.getRegionNameAsString(), serverName,
           column.getNameAsString(), e);
     }
@@ -345,7 +417,7 @@ public class CanaryTool implements Tool, Canary {
       return this.writeLatency;
     }
 
-    public Map<String, List<RegionTaskResult>> getRegionMap() {
+    public ConcurrentMap<String, List<RegionTaskResult>> getRegionMap() {
       return this.regionMap;
     }
 
@@ -908,6 +980,7 @@ public class CanaryTool implements Tool, Canary {
       System.arraycopy(args, index, monitorTargets, 0, length);
     }
 
+    putUpWebUI();
     if (zookeeperMode) {
       return checkZooKeeper();
     } else if (regionServerMode) {
@@ -1352,6 +1425,7 @@ public class CanaryTool implements Tool, Canary {
         try {
           List<Future<Void>> taskFutures = new LinkedList<>();
           RegionStdOutSink regionSink = this.getSink();
+          regionSink.resetFailuresCountDetails();
           if (this.targets != null && this.targets.length > 0) {
             String[] tables = generateMonitorTables(this.targets);
             // Check to see that each table name passed in the -readTableTimeouts argument is also
diff --git a/hbase-server/src/main/resources/hbase-webapps/canary/canary.jsp b/hbase-server/src/main/resources/hbase-webapps/canary/canary.jsp
new file mode 100644
index 0000000..2648ddd
--- /dev/null
+++ b/hbase-server/src/main/resources/hbase-webapps/canary/canary.jsp
@@ -0,0 +1,20 @@
+<%--
+/**
+ * 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.
+ */
+--%>
+<meta HTTP-EQUIV="REFRESH" content="0;url=/canary-status"/>
diff --git a/hbase-server/src/main/resources/hbase-webapps/canary/index.html b/hbase-server/src/main/resources/hbase-webapps/canary/index.html
new file mode 100644
index 0000000..7e03fdd
--- /dev/null
+++ b/hbase-server/src/main/resources/hbase-webapps/canary/index.html
@@ -0,0 +1,20 @@
+<!--
+/**
+ * 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.
+ */
+-->
+<meta HTTP-EQUIV="REFRESH" content="0;url=/canary-status"/>
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/tool/TestCanaryStatusServlet.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/tool/TestCanaryStatusServlet.java
new file mode 100644
index 0000000..56c02a5
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/tool/TestCanaryStatusServlet.java
@@ -0,0 +1,116 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.hadoop.hbase.tool;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.client.RegionInfoBuilder;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.tmpl.tool.CanaryStatusTmpl;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+
+@Category({ SmallTests.class })
+public class TestCanaryStatusServlet {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestCanaryStatusServlet.class);
+
+  @Test
+  public void testFailures() throws IOException {
+    CanaryTool.RegionStdOutSink regionStdOutSink = new CanaryTool.RegionStdOutSink();
+
+    ServerName serverName1 = ServerName.valueOf("staging-st04.server:22600",
+      1584180761635L);
+    TableName fakeTableName1 = TableName.valueOf("fakeTableName1");
+    RegionInfo regionInfo1 = RegionInfoBuilder.newBuilder(fakeTableName1).build();
+
+    ServerName serverName2 = ServerName.valueOf("staging-st05.server:22600",
+      1584180761636L);
+    TableName fakeTableName2 = TableName.valueOf("fakeTableName2");
+    RegionInfo regionInfo2 = RegionInfoBuilder.newBuilder(fakeTableName2).build();
+
+    regionStdOutSink.publishReadFailure(serverName1, regionInfo1, new IOException());
+    regionStdOutSink.publishWriteFailure(serverName2, regionInfo2, new IOException());
+    CanaryStatusTmpl tmpl = new CanaryStatusTmpl();
+    StringWriter renderResultWriter = new StringWriter();
+    tmpl.render(renderResultWriter, regionStdOutSink);
+    String renderResult = renderResultWriter.toString();
+    Assert.assertTrue(renderResult.contains("staging-st04.server,22600"));
+    Assert.assertTrue(renderResult.contains("fakeTableName1"));
+    Assert.assertTrue(renderResult.contains("staging-st05.server,22600"));
+    Assert.assertTrue(renderResult.contains("fakeTableName2"));
+
+  }
+
+  @Test
+  public void testReadFailuresOnly() throws IOException {
+    CanaryTool.RegionStdOutSink regionStdOutSink = new CanaryTool.RegionStdOutSink();
+
+    ServerName serverName1 = ServerName.valueOf("staging-st04.server:22600",
+      1584180761635L);
+    TableName fakeTableName1 = TableName.valueOf("fakeTableName1");
+    RegionInfo regionInfo1 = RegionInfoBuilder.newBuilder(fakeTableName1).build();
+
+    regionStdOutSink.publishReadFailure(serverName1, regionInfo1, new IOException());
+    CanaryStatusTmpl tmpl = new CanaryStatusTmpl();
+    StringWriter renderResultWriter = new StringWriter();
+    tmpl.render(renderResultWriter, regionStdOutSink);
+    String renderResult = renderResultWriter.toString();
+    Assert.assertTrue(renderResult.contains("staging-st04.server,22600"));
+    Assert.assertTrue(renderResult.contains("fakeTableName1"));
+  }
+
+  @Test
+  public void testWriteFailuresOnly() throws IOException {
+    CanaryTool.RegionStdOutSink regionStdOutSink = new CanaryTool.RegionStdOutSink();
+
+    ServerName serverName2 = ServerName.valueOf("staging-st05.server:22600",
+      1584180761636L);
+    TableName fakeTableName2 = TableName.valueOf("fakeTableName2");
+    RegionInfo regionInfo2 = RegionInfoBuilder.newBuilder(fakeTableName2).build();
+
+    regionStdOutSink.publishReadFailure(serverName2, regionInfo2, new IOException());
+    CanaryStatusTmpl tmpl = new CanaryStatusTmpl();
+    StringWriter renderResultWriter = new StringWriter();
+    tmpl.render(renderResultWriter, regionStdOutSink);
+    String renderResult = renderResultWriter.toString();
+    Assert.assertTrue(renderResult.contains("staging-st05.server,22600"));
+    Assert.assertTrue(renderResult.contains("fakeTableName2"));
+
+  }
+
+  @Test
+  public void testNoFailures() throws IOException {
+    CanaryTool.RegionStdOutSink regionStdOutSink = new CanaryTool.RegionStdOutSink();
+    CanaryStatusTmpl tmpl = new CanaryStatusTmpl();
+    StringWriter renderResultWriter = new StringWriter();
+    tmpl.render(renderResultWriter, regionStdOutSink);
+    String renderResult = renderResultWriter.toString();
+    Assert.assertTrue(renderResult.contains("Total Failed Servers: 0"));
+    Assert.assertTrue(renderResult.contains("Total Failed Tables: 0"));
+  }
+
+}