]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
ceph.in: add 'daemonperf' command
authorJohn Spray <john.spray@redhat.com>
Tue, 3 Feb 2015 13:13:29 +0000 (13:13 +0000)
committerJohn Spray <john.spray@redhat.com>
Thu, 5 Mar 2015 20:17:35 +0000 (20:17 +0000)
This is inspired by dstat and scripts/perf-watch.py, to
give a convenient live view of an interesting subset
of the performance counters from a Ceph daemon.

Signed-off-by: John Spray <john.spray@redhat.com>
ceph.spec.in
debian/ceph.install
src/Makefile.am
src/ceph.in
src/pybind/ceph_daemon.py [new file with mode: 0755]

index 567586cdb0b41a82ba5a02b34b5d738cf1b5b991..d28484406b7566bb9113fa0e00eee8666fedc961 100644 (file)
@@ -665,6 +665,7 @@ fi
 %config(noreplace) %{_sysconfdir}/ceph/rbdmap
 %{_initrddir}/rbdmap
 %{python_sitelib}/ceph_argparse.py*
+%{python_sitelib}/ceph_daemon.py*
 %if 0%{?rhel} >= 7 || 0%{?fedora}
 /usr/lib/udev/rules.d/50-rbd.rules
 %else
index 4923bbc59502443182e9c11ef6eb7bb66e3e3397..0cd2666a96ceab154aa65e18998c0ff7f48062dc 100644 (file)
@@ -33,3 +33,4 @@ usr/share/man/man8/crushtool.8
 usr/share/man/man8/monmaptool.8
 usr/share/man/man8/osdmaptool.8
 usr/lib/python*/dist-packages/ceph_argparse.py*
+usr/lib/python*/dist-packages/ceph_daemon.py*
index 57cccfc8ab017b04635ac6412260e2ed52de16d6..116be9d32e81a50658f413cbc3a7dccc45cb54e7 100644 (file)
@@ -727,6 +727,7 @@ python_PYTHON = pybind/rados.py \
                pybind/rbd.py \
                pybind/cephfs.py \
                pybind/ceph_argparse.py \
+               pybind/ceph_daemon.py \
                pybind/ceph_rest_api.py
 
 
index b8ca53c6e56e88ceb893425a87212244b7df2c5d..e56ff85dd018a4a1552c8f57feed373023db60ea 100755 (executable)
@@ -73,6 +73,8 @@ from ceph_argparse import \
     matchnum, validate_command, find_cmd_target, \
     send_command, json_command
 
+from ceph_daemon import DaemonWatcher, admin_socket
+
 # just a couple of globals
 
 verbose = False
@@ -303,60 +305,6 @@ def format_help(cmddict, partial=None):
 
     return fullusage
 
-def admin_socket(asok_path, cmd, format=''):
-    """
-    Send a daemon (--admin-daemon) command 'cmd'.  asok_path is the
-    path to the admin socket; cmd is a list of strings; format may be
-    set to one of the formatted forms to get output in that form
-    (daemon commands don't support 'plain' output).
-    """
-
-    def do_sockio(path, cmd):
-        """ helper: do all the actual low-level stream I/O """
-        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        sock.connect(path)
-        try:
-            sock.sendall(cmd + '\0')
-            len_str = sock.recv(4)
-            if len(len_str) < 4:
-                raise RuntimeError("no data returned from admin socket")
-            l, = struct.unpack(">I", len_str)
-            ret = ''
-
-            got = 0
-            while got < l:
-                bit = sock.recv(l - got)
-                ret += bit
-                got += len(bit)
-
-        except Exception as e:
-            raise RuntimeError('exception: ' + str(e))
-        return ret
-
-    try:
-        cmd_json = do_sockio(asok_path,
-            json.dumps({"prefix":"get_command_descriptions"}))
-    except Exception as e:
-        raise RuntimeError('exception getting command descriptions: ' + str(e))
-
-    if cmd == 'get_command_descriptions':
-        return cmd_json
-
-    sigdict = parse_json_funcsigs(cmd_json, 'cli')
-    valid_dict = validate_command(sigdict, cmd)
-    if not valid_dict:
-        raise RuntimeError('invalid command')
-
-    if format:
-        valid_dict['format'] = format
-
-    try:
-        ret = do_sockio(asok_path, json.dumps(valid_dict))
-    except Exception as e:
-        raise RuntimeError('exception: ' + str(e))
-
-    return ret
-
 
 def ceph_conf(parsed_args, field, name):
     args=['ceph-conf']
@@ -573,12 +521,16 @@ def main():
 
     format = parsed_args.output_format
 
+    daemon_perf = False
     sockpath = None
     if parsed_args.admin_socket:
         sockpath = parsed_args.admin_socket
-    elif len(childargs) > 0 and childargs[0] == "daemon":
+    elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
+        daemon_perf = (childargs[0] == "daemonperf")
         # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
-        if len(childargs) > 2:
+        # Handle "daemonperf <path>" the same but requires no trailing args
+        require_args = 2 if daemon_perf else 3
+        if len(childargs) >= require_args:
             if childargs[1].find('/') >= 0:
                 sockpath = childargs[1]
             else:
@@ -596,7 +548,10 @@ def main():
             print >> sys.stderr, 'daemon requires at least 3 arguments'
             return errno.EINVAL
 
-    if sockpath:
+    if sockpath and daemon_perf:
+        DaemonWatcher(sockpath).run()
+        return 0
+    elif sockpath:
         try:
             print admin_socket(sockpath, childargs, format)
         except Exception as e:
diff --git a/src/pybind/ceph_daemon.py b/src/pybind/ceph_daemon.py
new file mode 100755 (executable)
index 0000000..30c54e4
--- /dev/null
@@ -0,0 +1,274 @@
+# -*- mode:python -*-
+# vim: ts=4 sw=4 smarttab expandtab
+
+"""
+Copyright (C) 2015 Red Hat
+
+This is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public
+License version 2, as published by the Free Software
+Foundation.  See file COPYING.
+"""
+
+import sys
+import json
+import socket
+import struct
+import time
+from collections import defaultdict
+
+from ceph_argparse import parse_json_funcsigs, validate_command
+
+COUNTER = 0x8
+LONG_RUNNING_AVG = 0x4
+
+
+def admin_socket(asok_path, cmd, format=''):
+    """
+    Send a daemon (--admin-daemon) command 'cmd'.  asok_path is the
+    path to the admin socket; cmd is a list of strings; format may be
+    set to one of the formatted forms to get output in that form
+    (daemon commands don't support 'plain' output).
+    """
+
+    def do_sockio(path, cmd_bytes):
+        """ helper: do all the actual low-level stream I/O """
+        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        sock.connect(path)
+        try:
+            sock.sendall(cmd_bytes + '\0')
+            len_str = sock.recv(4)
+            if len(len_str) < 4:
+                raise RuntimeError("no data returned from admin socket")
+            l, = struct.unpack(">I", len_str)
+            sock_ret = ''
+
+            got = 0
+            while got < l:
+                bit = sock.recv(l - got)
+                sock_ret += bit
+                got += len(bit)
+
+        except Exception as sock_e:
+            raise RuntimeError('exception: ' + str(sock_e))
+        return sock_ret
+
+    try:
+        cmd_json = do_sockio(asok_path,
+                             json.dumps({"prefix": "get_command_descriptions"}))
+    except Exception as e:
+        raise RuntimeError('exception getting command descriptions: ' + str(e))
+
+    if cmd == 'get_command_descriptions':
+        return cmd_json
+
+    sigdict = parse_json_funcsigs(cmd_json, 'cli')
+    valid_dict = validate_command(sigdict, cmd)
+    if not valid_dict:
+        raise RuntimeError('invalid command')
+
+    if format:
+        valid_dict['format'] = format
+
+    try:
+        ret = do_sockio(asok_path, json.dumps(valid_dict))
+    except Exception as e:
+        raise RuntimeError('exception: ' + str(e))
+
+    return ret
+
+
+class DaemonWatcher(object):
+    """
+    Given a Ceph daemon's admin socket path, poll its performance counters
+    and output a series of output lines showing the momentary values of
+    counters of interest (those with the 'nick' property in Ceph's schema)
+    """
+    (
+        BLACK,
+        RED,
+        GREEN,
+        YELLOW,
+        BLUE,
+        MAGENTA,
+        CYAN,
+        GRAY
+    ) = range(8)
+
+    RESET_SEQ = "\033[0m"
+    COLOR_SEQ = "\033[1;%dm"
+    COLOR_DARK_SEQ = "\033[0;%dm"
+    BOLD_SEQ = "\033[1m"
+    UNDERLINE_SEQ = "\033[4m"
+
+    def __init__(self, asok):
+        self.asok_path = asok
+        self._colored = False
+
+        self._stats = None
+        self._schema = None
+
+    def supports_color(self, ostr):
+        """
+        Returns True if the running system's terminal supports color, and False
+        otherwise.
+        """
+        unsupported_platform = (sys.platform in ('win32', 'Pocket PC'))
+        # isatty is not always implemented, #6223.
+        is_a_tty = hasattr(ostr, 'isatty') and ostr.isatty()
+        if unsupported_platform or not is_a_tty:
+            return False
+        return True
+
+    def colorize(self, msg, color, dark=False):
+        """
+        Decorate `msg` with escape sequences to give the requested color
+        """
+        return (self.COLOR_DARK_SEQ if dark else self.COLOR_SEQ) % (30 + color) \
+               + msg + self.RESET_SEQ
+
+    def bold(self, msg):
+        """
+        Decorate `msg` with escape sequences to make it appear bold
+        """
+        return self.BOLD_SEQ + msg + self.RESET_SEQ
+
+    def format_dimless(self, n, width):
+        """
+        Format a number without units, so as to fit into `width` characters, substituting
+        an appropriate unit suffix.
+        """
+        units = [' ', 'k', 'M', 'G', 'T', 'P']
+        unit = 0
+        while len("%s" % (int(n) / (1000**unit))) > width - 1:
+            unit += 1
+
+        if unit > 0:
+            truncated_float = ("%f" % (n / (1000.0 ** unit)))[0:width - 1]
+            if truncated_float[-1] == '.':
+                truncated_float = " " + truncated_float[0:-1]
+        else:
+            truncated_float = "%{wid}d".format(wid=width-1) % n
+        formatted = "%s%s" % (truncated_float, units[unit])
+
+        if self._colored:
+            if n == 0:
+                color = self.BLACK, False
+            else:
+                color = self.YELLOW, False
+            return self.bold(self.colorize(formatted[0:-1], color[0], color[1])) \
+                + self.bold(self.colorize(formatted[-1], self.BLACK, False))
+        else:
+            return formatted
+
+    def col_width(self, nick):
+        """
+        Given the short name `nick` for a column, how many characters
+        of width should the column be allocated?  Does not include spacing
+        between columns.
+        """
+        return max(len(nick), 4)
+
+    def _print_headers(self, ostr):
+        """
+        Print a header row to `ostr`
+        """
+        header = ""
+        for section_name, names in self._stats.items():
+            section_width = sum([self.col_width(x)+1 for x in names.values()]) - 1
+            pad = max(section_width - len(section_name), 0)
+            pad_prefix = pad / 2
+            header += (pad_prefix * '-')
+            header += (section_name[0:section_width])
+            header += ((pad - pad_prefix) * '-')
+            header += ' '
+        header += "\n"
+        ostr.write(self.colorize(header, self.BLUE, True))
+
+        sub_header = ""
+        for section_name, names in self._stats.items():
+            for stat_name, stat_nick in names.items():
+                sub_header += self.UNDERLINE_SEQ \
+                              + self.colorize(
+                                    stat_nick.ljust(self.col_width(stat_nick)),
+                                    self.BLUE) \
+                              + ' '
+            sub_header = sub_header[0:-1] + self.colorize('|', self.BLUE)
+        sub_header += "\n"
+        ostr.write(sub_header)
+
+    def _print_vals(self, ostr, dump, last_dump):
+        """
+        Print a single row of values to `ostr`, based on deltas between `dump` and
+        `last_dump`.
+        """
+        val_row = ""
+        for section_name, names in self._stats.items():
+            for stat_name, stat_nick in names.items():
+                stat_type = self._schema[section_name][stat_name]['type']
+                if bool(stat_type & COUNTER):
+                    n = max(dump[section_name][stat_name] -
+                            last_dump[section_name][stat_name], 0)
+                elif bool(stat_type & LONG_RUNNING_AVG):
+                    entries = dump[section_name][stat_name]['avgcount'] - \
+                            last_dump[section_name][stat_name]['avgcount']
+                    if entries:
+                        n = (dump[section_name][stat_name]['sum'] -
+                             last_dump[section_name][stat_name]['sum']) \
+                            / float(entries)
+                        n *= 1000.0  # Present in milliseconds
+                    else:
+                        n = 0
+                else:
+                    n = dump[section_name][stat_name]
+
+                val_row += self.format_dimless(n, self.col_width(stat_nick))
+                val_row += " "
+            val_row = val_row[0:-1]
+            val_row += self.colorize("|", self.BLUE)
+        val_row = val_row[0:-len(self.colorize("|", self.BLUE))]
+        ostr.write("{0}\n".format(val_row))
+
+    def _load_schema(self):
+        """
+        Populate our instance-local copy of the daemon's performance counter
+        schema, and work out which stats we will display.
+        """
+        self._schema = json.loads(admin_socket(self.asok_path, ["perf", "schema"]))
+
+        # Build list of which stats we will display, based on which
+        # stats have a nickname
+        self._stats = defaultdict(dict)
+        for section_name, section_stats in self._schema.items():
+            for name, schema_data in section_stats.items():
+                if schema_data.get('nick'):
+                    self._stats[section_name][name] = schema_data['nick']
+
+    def run(self, ostr=sys.stdout):
+        """
+        Print output at regular intervals until interrupted.
+
+        :param ostr: Stream to which to send output
+        """
+
+        self._load_schema()
+        self._colored = self.supports_color(ostr)
+
+        self._print_headers(ostr)
+
+        last_dump = json.loads(admin_socket(self.asok_path, ["perf", "dump"]))
+        rows_since_header = 0
+        term_height = 25
+
+        try:
+            while True:
+                dump = json.loads(admin_socket(self.asok_path, ["perf", "dump"]))
+                if rows_since_header > term_height - 2:
+                    self._print_headers(ostr)
+                    rows_since_header = 0
+                self._print_vals(ostr, dump, last_dump)
+                rows_since_header += 1
+                last_dump = dump
+                time.sleep(1)
+        except KeyboardInterrupt:
+            return