<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[15702] CalendarServer/trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/15702">15702</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2016-06-23 19:17:34 -0700 (Thu, 23 Jun 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Allow dashview to replay data from a previously captured dashcollect log file.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertoolsdashviewpy">CalendarServer/trunk/calendarserver/tools/dashview.py</a></li>
<li><a href="#CalendarServertrunkdocAdminDashboardmd">CalendarServer/trunk/doc/Admin/Dashboard.md</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertoolsdashviewpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/dashview.py (15701 => 15702)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/dashview.py        2016-06-24 00:41:14 UTC (rev 15701)
+++ CalendarServer/trunk/calendarserver/tools/dashview.py        2016-06-24 02:17:34 UTC (rev 15702)
</span><span class="lines">@@ -14,22 +14,23 @@
</span><span class="cx"> # See the License for the specific language governing permissions and
</span><span class="cx"> # limitations under the License.
</span><span class="cx"> ##
</span><del>-from argparse import HelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE, \
-    ArgumentParser
</del><span class="cx"> 
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> A curses (or plain text) based dashboard for viewing various aspects of the
</span><span class="cx"> server as exposed by the L{DashboardProtocol} stats socket.
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-from collections import OrderedDict
</del><ins>+from argparse import HelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE, \
+    ArgumentParser
+from collections import OrderedDict, defaultdict
</ins><span class="cx"> from operator import itemgetter
</span><del>-import collections
</del><ins>+from zlib import decompress
</ins><span class="cx"> import curses.panel
</span><span class="cx"> import errno
</span><span class="cx"> import fcntl
</span><span class="cx"> import json
</span><span class="cx"> import logging
</span><ins>+import os
</ins><span class="cx"> import sched
</span><span class="cx"> import socket
</span><span class="cx"> import struct
</span><span class="lines">@@ -67,21 +68,29 @@
</span><span class="cx">     parser = ArgumentParser(
</span><span class="cx">         formatter_class=MyHelpFormatter,
</span><span class="cx">         description=&quot;Dashboard collector viewer service for CalendarServer.&quot;,
</span><ins>+        epilog=&quot;One of -s or -l should be specified&quot;,
</ins><span class="cx">     )
</span><del>-    parser.add_argument(&quot;-s&quot;, default=&quot;localhost:8200&quot;, help=&quot;Dashboard collector service host:port&quot;)
</del><ins>+    group = parser.add_mutually_exclusive_group()
+    group.add_argument(&quot;-s&quot;, default=&quot;localhost:8200&quot;, help=&quot;Dashboard collector service host:port&quot;)
+    group.add_argument(&quot;-l&quot;, default=SUPPRESS, help=&quot;Dashboard collector log file to replay data from&quot;)
</ins><span class="cx">     args = parser.parse_args()
</span><span class="cx"> 
</span><span class="cx">     #
</span><span class="cx">     # Get configuration
</span><span class="cx">     #
</span><del>-    server = args.s
-    if not server.startswith(&quot;unix:&quot;):
-        server = server.split(&quot;:&quot;)
-        if len(server) == 1:
-            server.append(8100)
-        else:
-            server[1] = int(server[1])
-        server = tuple(server)
</del><ins>+    if hasattr(args, &quot;l&quot;):
+        server = None
+        logfile = open(os.path.expanduser(args.l))
+    else:
+        server = args.s
+        if not server.startswith(&quot;unix:&quot;):
+            server = server.split(&quot;:&quot;)
+            if len(server) == 1:
+                server.append(8100)
+            else:
+                server[1] = int(server[1])
+            server = tuple(server)
+        logfile = None
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def _wrapped(stdscrn):
</span><span class="lines">@@ -91,7 +100,7 @@
</span><span class="cx">             curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)
</span><span class="cx">         except:
</span><span class="cx">             pass
</span><del>-        d = Dashboard(server, stdscrn)
</del><ins>+        d = Dashboard(server, logfile, stdscrn)
</ins><span class="cx">         d.run()
</span><span class="cx">     curses.wrapper(_wrapped)
</span><span class="cx"> 
</span><span class="lines">@@ -99,8 +108,13 @@
</span><span class="cx"> #    client.getOneItem(&quot;podA&quot;, &quot;localhost:8100&quot;, &quot;jobcount&quot;)
</span><span class="cx"> #    print(json.dumps(client.currentData[&quot;pods&quot;][&quot;podA&quot;][&quot;aggregate&quot;], indent=1))
</span><span class="cx"> 
</span><ins>+#    dashboard = Dashboard(server, logfile, None)
+#    client = DashboardLogfile(dashboard, logfile)
+#    client.getOneItem(&quot;podA&quot;, &quot;localhost:8100&quot;, &quot;jobcount&quot;)
+#    print(json.dumps(client.currentData[&quot;pods&quot;][&quot;podA&quot;][&quot;cal-prod-01.pixar.com:8100&quot;], indent=1))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> def safeDivision(value, total, factor=1):
</span><span class="cx">     return value * factor / total if total else 0
</span><span class="cx"> 
</span><span class="lines">@@ -130,14 +144,14 @@
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">     screen = None
</span><del>-    registered_windows = collections.OrderedDict()
</del><ins>+    registered_windows = OrderedDict()
</ins><span class="cx">     registered_window_sets = {
</span><span class="cx">         &quot;D&quot;: (&quot;Directory Panels&quot;, [],),
</span><span class="cx">         &quot;H&quot;: (&quot;HTTP Panels&quot;, [],),
</span><span class="cx">         &quot;J&quot;: (&quot;Jobs Panels&quot;, [],),
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    def __init__(self, server, screen):
</del><ins>+    def __init__(self, server, logfile, screen):
</ins><span class="cx">         self.screen = screen
</span><span class="cx">         self.paused = False
</span><span class="cx">         self.seconds = 1.0
</span><span class="lines">@@ -147,7 +161,10 @@
</span><span class="cx">         self.selected_server = Point()
</span><span class="cx">         self.server_window = None
</span><span class="cx"> 
</span><del>-        self.client = DashboardClient(self, server)
</del><ins>+        if server:
+            self.client = DashboardClient(self, server)
+        else:
+            self.client = DashboardLogfile(self, logfile)
</ins><span class="cx">         self.client_error = False
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -266,7 +283,8 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Periodic update of the current window and check for a key press.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        self.client.update()
</del><ins>+        if not self.paused:
+            self.client.update()
</ins><span class="cx">         client_error = len(self.client.currentData) == 0
</span><span class="cx">         if client_error ^ self.client_error:
</span><span class="cx">             self.client_error = client_error
</span><span class="lines">@@ -354,8 +372,12 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def timestamp(self):
+        return self.client.currentData[&quot;timestamp&quot;]
+
+
</ins><span class="cx">     def pods(self):
</span><del>-        return self.client.currentData.get(&quot;pods&quot;, {}).keys()
</del><ins>+        return sorted(self.client.currentData.get(&quot;pods&quot;, {}).keys())
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def selectedPod(self):
</span><span class="lines">@@ -363,7 +385,7 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def serversForPod(self, pod):
</span><del>-        return self.client.currentData.get(&quot;pods&quot;, {pod: {}})[pod].keys()
</del><ins>+        return sorted(self.client.currentData.get(&quot;pods&quot;, {pod: {}})[pod].keys())
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def selectedServer(self):
</span><span class="lines">@@ -371,58 +393,21 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class DashboardClient(object):
</del><ins>+class BaseDashboardClient(object):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    Client that connects to a server and fetches information.
</del><ins>+    Base class for clients that get dashboard data.
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-    def __init__(self, dashboard, sockname):
</del><ins>+    def __init__(self, dashboard):
</ins><span class="cx">         self.dashboard = dashboard
</span><del>-        self.socket = None
-        if isinstance(sockname, str):
-            self.sockname = sockname[5:]
-            self.useTCP = False
-        else:
-            self.sockname = sockname
-            self.useTCP = True
</del><span class="cx">         self.currentData = {}
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def readSock(self):
</del><ins>+    def readData(self):
</ins><span class="cx">         &quot;&quot;&quot;
</span><del>-        Open a socket, send the specified request, and retrieve the response. The socket closes.
</del><ins>+        Get the data
</ins><span class="cx">         &quot;&quot;&quot;
</span><del>-        try:
-            self.socket = socket.socket(socket.AF_INET if self.useTCP else socket.AF_UNIX, socket.SOCK_STREAM)
-            self.socket.connect(self.sockname)
-            self.socket.setblocking(0)
-            data = &quot;&quot;
-            t = time.time()
-            while not data.endswith(&quot;\n&quot;):
-                try:
-                    d = self.socket.recv(1024)
-                except socket.error as se:
-                    if se.args[0] != errno.EWOULDBLOCK:
-                        raise
-                    if time.time() - t &gt; 5:
-                        raise socket.error
-                    continue
-                if d:
-                    data += d
-                else:
-                    break
-            data = json.loads(data, object_pairs_hook=collections.OrderedDict)
-            logging.debug(&quot;data: {}&quot;.format(len(data)))
-            self.socket.close()
-            self.socket = None
-        except socket.error as e:
-            data = {}
-            self.socket = None
-            logging.debug(&quot;readSock: failed: {}&quot;.format(e))
-        except ValueError as e:
-            data = {}
-            logging.debug(&quot;readSock: failed: {}&quot;.format(e))
-        return data
</del><ins>+        raise NotImplementedError
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def update(self):
</span><span class="lines">@@ -431,7 +416,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Only read each item once
</span><del>-        self.currentData = self.readSock()
</del><ins>+        self.currentData = self.readData()
</ins><span class="cx">         if self.dashboard.aggregate:
</span><span class="cx">             self.aggregateData()
</span><span class="cx"> 
</span><span class="lines">@@ -447,7 +432,7 @@
</span><span class="cx">         # it would be too expensive to run the DB query for all servers. So when we
</span><span class="cx">         # need the jobs data, always substitute the first server's data
</span><span class="cx">         if item in (&quot;jobs&quot;, &quot;jobcount&quot;):
</span><del>-            server = self.currentData[&quot;pods&quot;][pod].keys()[0]
</del><ins>+            server = sorted(self.currentData[&quot;pods&quot;][pod].keys())[0]
</ins><span class="cx"> 
</span><span class="cx">         return self.currentData[&quot;pods&quot;][pod][server].get(item)
</span><span class="cx"> 
</span><span class="lines">@@ -476,7 +461,7 @@
</span><span class="cx">         results = OrderedDict()
</span><span class="cx"> 
</span><span class="cx">         # Get all items available in all servers first
</span><del>-        items = collections.defaultdict(list)
</del><ins>+        items = defaultdict(list)
</ins><span class="cx">         for server in data.keys():
</span><span class="cx">             for item in data[server].keys():
</span><span class="cx">                 items[item].append(data[server][item])
</span><span class="lines">@@ -495,6 +480,88 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+class DashboardClient(BaseDashboardClient):
+    &quot;&quot;&quot;
+    Client that connects to a server and fetches information.
+    &quot;&quot;&quot;
+
+    def __init__(self, dashboard, sockname):
+        super(DashboardClient, self).__init__(dashboard)
+        self.socket = None
+        if isinstance(sockname, str):
+            self.sockname = sockname[5:]
+            self.useTCP = False
+        else:
+            self.sockname = sockname
+            self.useTCP = True
+
+
+    def readData(self):
+        &quot;&quot;&quot;
+        Open a socket, send the specified request, and retrieve the response. The socket closes.
+        &quot;&quot;&quot;
+        try:
+            self.socket = socket.socket(socket.AF_INET if self.useTCP else socket.AF_UNIX, socket.SOCK_STREAM)
+            self.socket.connect(self.sockname)
+            self.socket.setblocking(0)
+            data = &quot;&quot;
+            t = time.time()
+            while not data.endswith(&quot;\n&quot;):
+                try:
+                    d = self.socket.recv(1024)
+                except socket.error as se:
+                    if se.args[0] != errno.EWOULDBLOCK:
+                        raise
+                    if time.time() - t &gt; 5:
+                        raise socket.error
+                    continue
+                if d:
+                    data += d
+                else:
+                    break
+            data = json.loads(data, object_pairs_hook=OrderedDict)
+            logging.debug(&quot;data: {}&quot;.format(len(data)))
+            self.socket.close()
+            self.socket = None
+        except socket.error as e:
+            data = {}
+            self.socket = None
+            logging.debug(&quot;readData: failed: {}&quot;.format(e))
+        except ValueError as e:
+            data = {}
+            logging.debug(&quot;readData: failed: {}&quot;.format(e))
+        return data
+
+
+
+class DashboardLogfile(BaseDashboardClient):
+    &quot;&quot;&quot;
+    Client that gets data from a log file.
+    &quot;&quot;&quot;
+
+    def __init__(self, dashboard, logfile):
+        super(DashboardLogfile, self).__init__(dashboard)
+        self.logfile = logfile
+
+
+    def readData(self):
+        &quot;&quot;&quot;
+        Read one line of data from the logile
+        &quot;&quot;&quot;
+        try:
+            line = self.logfile.readline()
+            if line[0] == &quot;\x1e&quot;:
+                line = line[1:]
+            if line[0] != &quot;{&quot;:
+                line = decompress(line.decode(&quot;base64&quot;))
+            data = json.loads(line)
+        except ValueError as e:
+            data = {}
+            logging.debug(&quot;readData: failed: {}&quot;.format(e))
+        return data
+
+
+
</ins><span class="cx"> class Aggregator(object):
</span><span class="cx"> 
</span><span class="cx">     @staticmethod
</span><span class="lines">@@ -924,11 +991,11 @@
</span><span class="cx">     clientItem = &quot;stats_system&quot;
</span><span class="cx"> 
</span><span class="cx">     windowTitle = &quot;System&quot;
</span><del>-    formatWidth = 52
</del><ins>+    formatWidth = 54
</ins><span class="cx">     additionalRows = 3
</span><span class="cx"> 
</span><span class="cx">     def updateRowCount(self):
</span><del>-        self.rowCount = len(defaultIfNone(self.clientData(), (1, 2, 3, 4,)))
</del><ins>+        self.rowCount = len(defaultIfNone(self.clientData(), (1, 2, 3, 4,))) + 1
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def update(self):
</span><span class="lines">@@ -938,12 +1005,14 @@
</span><span class="cx">             &quot;memory used&quot;: 0,
</span><span class="cx">             &quot;start time&quot;: time.time(),
</span><span class="cx">         })
</span><ins>+        records[&quot;timestamp&quot;] = self.dashboard.timestamp()
+
</ins><span class="cx">         if len(records) != self.rowCount:
</span><span class="cx">             self.needsReset = True
</span><span class="cx">             return
</span><span class="cx">         self.iter += 1
</span><span class="cx"> 
</span><del>-        s = &quot; {:&lt;30}{:&gt;18} &quot;.format(&quot;Item&quot;, &quot;Value&quot;)
</del><ins>+        s = &quot; {:&lt;30}{:&gt;20} &quot;.format(&quot;Item&quot;, &quot;Value&quot;)
</ins><span class="cx">         pt = self.tableHeader((s,), len(records))
</span><span class="cx"> 
</span><span class="cx">         records[&quot;cpu use&quot;] = &quot;{:.2f}&quot;.format(records[&quot;cpu use&quot;])
</span><span class="lines">@@ -955,12 +1024,13 @@
</span><span class="cx">         hours, mins = divmod(records[&quot;uptime&quot;] / 60, 60)
</span><span class="cx">         records[&quot;uptime&quot;] = &quot;{}:{:02d} hh:mm&quot;.format(hours, mins)
</span><span class="cx">         del records[&quot;start time&quot;]
</span><ins>+        records[&quot;timestamp&quot;] = records[&quot;timestamp&quot;].replace(&quot;T&quot;, &quot; &quot;)
</ins><span class="cx"> 
</span><span class="cx">         for item, value in sorted(records.items(), key=lambda x: x[0]):
</span><span class="cx">             changed = (
</span><span class="cx">                 item in self.lastResult and self.lastResult[item] != value
</span><span class="cx">             )
</span><del>-            s = &quot; {:&lt;30}{:&gt;18} &quot;.format(item, value)
</del><ins>+            s = &quot; {:&lt;30}{:&gt;20} &quot;.format(item, value)
</ins><span class="cx">             self.tableRow(
</span><span class="cx">                 s, pt,
</span><span class="cx">                 curses.A_REVERSE if changed else curses.A_NORMAL,
</span></span></pre></div>
<a id="CalendarServertrunkdocAdminDashboardmd"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/doc/Admin/Dashboard.md (15701 => 15702)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/doc/Admin/Dashboard.md        2016-06-24 00:41:14 UTC (rev 15701)
+++ CalendarServer/trunk/doc/Admin/Dashboard.md        2016-06-24 02:17:34 UTC (rev 15702)
</span><span class="lines">@@ -104,19 +104,24 @@
</span><span class="cx"> 
</span><span class="cx"> ### `dashview` tool
</span><span class="cx"> 
</span><del>-The `dashview` tool is a command line tool that periodically reads from a `dashcollect` socket and displays the data in a curses-based terminal using different table views for each class of data. The user can control which tables are visible at any time. The tool can show the data for any host in a multi-pod/multi-host CalendarServer service, and in addition can show the aggregated data for all hosts in a pod. This tool typically requires a large terminal window for viewing, and the terminal will need good curses support. This tool replaces the older `dashboard` tool which read stats directly from the CalendarServer hosts and is now considered deprecated since having multiple users using it causes service performance issues.
</del><ins>+The `dashview` tool is a command line tool that periodically reads from a `dashcollect` socket and displays the data in a curses-based terminal using different table views for each class of data. Alternatively, the tool can read and display data from a `dashcollect` log file so that data captured earlier can be replayed. The user can control which tables are visible at any time. The tool can show the data for any host in a multi-pod/multi-host CalendarServer service, and in addition can show the aggregated data for all hosts in a pod. This tool typically requires a large terminal window for viewing, and the terminal will need good curses support. This tool replaces the older `dashboard` tool which read stats directly from the CalendarServer hosts and is now considered deprecated since having multiple users using it causes service performance issues.
</ins><span class="cx"> 
</span><span class="cx"> #### Help
</span><span class="cx"> 
</span><del>-        usage: dashview.py [-h] [-s S]
</del><ins>+        dashview.py --help
+        usage: dashview.py [-h] [-s S | -l L]
</ins><span class="cx">         
</span><span class="cx">         Dashboard collector viewer service for CalendarServer.
</span><span class="cx">         
</span><span class="cx">         optional arguments:
</span><span class="cx">           -h, --help  show this help message and exit
</span><span class="cx">           -s S        Dashboard collector service host:port (default: localhost:8200)
</span><ins>+          -l L        Dashboard collector log file to replay data from
+        
+        One of -s or -l should be specified
</ins><span class="cx"> 
</span><span class="cx"> * The `-s` option specifies the `dashcollect` service host and port where JSON data can be read from.
</span><ins>+* The `-l` option specifies the `dashcollect` log file to read previously captured data from. 
</ins><span class="cx"> 
</span><span class="cx"> #### Panels
</span><span class="cx"> 
</span></span></pre>
</div>
</div>

</body>
</html>