2 # vim: ts=4 sw=4 smarttab expandtab
5 Copyright (C) 2015 Red Hat
7 This is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public
9 License version 2, as published by the Free Software
10 Foundation. See file COPYING.
18 from collections import OrderedDict
19 from fcntl import ioctl
20 from fnmatch import fnmatch
21 from prettytable import PrettyTable, HEADER
22 from signal import signal, SIGWINCH
23 from termios import TIOCGWINSZ
25 from ceph_argparse import parse_json_funcsigs, validate_command
28 LONG_RUNNING_AVG = 0x4
29 READ_CHUNK_SIZE = 4096
32 def admin_socket(asok_path, cmd, format=''):
34 Send a daemon (--admin-daemon) command 'cmd'. asok_path is the
35 path to the admin socket; cmd is a list of strings; format may be
36 set to one of the formatted forms to get output in that form
37 (daemon commands don't support 'plain' output).
40 def do_sockio(path, cmd_bytes):
41 """ helper: do all the actual low-level stream I/O """
42 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
45 sock.sendall(cmd_bytes + b'\0')
46 len_str = sock.recv(4)
48 raise RuntimeError("no data returned from admin socket")
49 l, = struct.unpack(">I", len_str)
54 # recv() receives signed int, i.e max 2GB
55 # workaround by capping READ_CHUNK_SIZE per call.
56 want = min(l - got, READ_CHUNK_SIZE)
61 except Exception as sock_e:
62 raise RuntimeError('exception: ' + str(sock_e))
66 cmd_json = do_sockio(asok_path,
67 b'{"prefix": "get_command_descriptions"}')
68 except Exception as e:
69 raise RuntimeError('exception getting command descriptions: ' + str(e))
71 if cmd == 'get_command_descriptions':
74 sigdict = parse_json_funcsigs(cmd_json.decode('utf-8'), 'cli')
75 valid_dict = validate_command(sigdict, cmd)
77 raise RuntimeError('invalid command')
80 valid_dict['format'] = format
83 ret = do_sockio(asok_path, json.dumps(valid_dict).encode('utf-8'))
84 except Exception as e:
85 raise RuntimeError('exception: ' + str(e))
90 class Termsize(object):
91 DEFAULT_SIZE = (25, 80)
93 self.rows, self.cols = self._gettermsize()
96 def _gettermsize(self):
98 fd = sys.stdin.fileno()
99 sz = struct.pack('hhhh', 0, 0, 0, 0)
100 rows, cols = struct.unpack('hhhh', ioctl(fd, TIOCGWINSZ, sz))[:2]
103 return self.DEFAULT_SIZE
106 rows, cols = self._gettermsize()
108 self.changed = (self.rows, self.cols) != (rows, cols)
109 self.rows, self.cols = rows, cols
111 def reset_changed(self):
115 return '%s(%dx%d, changed %s)' % (self.__class__,
116 self.rows, self.cols, self.changed)
119 return 'Termsize(%d,%d,%s)' % (self.__class__,
120 self.rows, self.cols, self.changed)
123 class DaemonWatcher(object):
125 Given a Ceph daemon's admin socket path, poll its performance counters
126 and output a series of output lines showing the momentary values of
127 counters of interest (those with the 'nick' property in Ceph's schema)
140 RESET_SEQ = "\033[0m"
141 COLOR_SEQ = "\033[1;%dm"
142 COLOR_DARK_SEQ = "\033[0;%dm"
144 UNDERLINE_SEQ = "\033[4m"
146 def __init__(self, asok, statpats=None, min_prio=0):
147 self.asok_path = asok
148 self._colored = False
152 self._statpats = statpats
153 self._stats_that_fit = dict()
154 self._min_prio = min_prio
155 self.termsize = Termsize()
157 def supports_color(self, ostr):
159 Returns True if the running system's terminal supports color, and False
162 unsupported_platform = (sys.platform in ('win32', 'Pocket PC'))
163 # isatty is not always implemented, #6223.
164 is_a_tty = hasattr(ostr, 'isatty') and ostr.isatty()
165 if unsupported_platform or not is_a_tty:
169 def colorize(self, msg, color, dark=False):
171 Decorate `msg` with escape sequences to give the requested color
173 return (self.COLOR_DARK_SEQ if dark else self.COLOR_SEQ) % (30 + color) \
174 + msg + self.RESET_SEQ
178 Decorate `msg` with escape sequences to make it appear bold
180 return self.BOLD_SEQ + msg + self.RESET_SEQ
182 def format_dimless(self, n, width):
184 Format a number without units, so as to fit into `width` characters, substituting
185 an appropriate unit suffix.
187 units = [' ', 'k', 'M', 'G', 'T', 'P']
189 while len("%s" % (int(n) // (1000**unit))) > width - 1:
193 truncated_float = ("%f" % (n / (1000.0 ** unit)))[0:width - 1]
194 if truncated_float[-1] == '.':
195 truncated_float = " " + truncated_float[0:-1]
197 truncated_float = "%{wid}d".format(wid=width-1) % n
198 formatted = "%s%s" % (truncated_float, units[unit])
202 color = self.BLACK, False
204 color = self.YELLOW, False
205 return self.bold(self.colorize(formatted[0:-1], color[0], color[1])) \
206 + self.bold(self.colorize(formatted[-1], self.BLACK, False))
210 def col_width(self, nick):
212 Given the short name `nick` for a column, how many characters
213 of width should the column be allocated? Does not include spacing
216 return max(len(nick), 4)
218 def get_stats_that_fit(self):
220 Get a possibly-truncated list of stats to display based on
221 current terminal width. Allow breaking mid-section.
223 current_fit = OrderedDict()
224 if self.termsize.changed or not self._stats_that_fit:
226 for section_name, names in self._stats.items():
227 for name, stat_data in names.items():
228 width += self.col_width(stat_data) + 1
229 if width > self.termsize.cols:
231 if section_name not in current_fit:
232 current_fit[section_name] = OrderedDict()
233 current_fit[section_name][name] = stat_data
234 if width > self.termsize.cols:
237 self.termsize.reset_changed()
238 changed = current_fit and (current_fit != self._stats_that_fit)
240 self._stats_that_fit = current_fit
241 return self._stats_that_fit, changed
243 def _print_headers(self, ostr):
245 Print a header row to `ostr`
248 stats, _ = self.get_stats_that_fit()
249 for section_name, names in stats.items():
251 sum([self.col_width(x) + 1 for x in names.values()]) - 1
252 pad = max(section_width - len(section_name), 0)
253 pad_prefix = pad // 2
254 header += (pad_prefix * '-')
255 header += (section_name[0:section_width])
256 header += ((pad - pad_prefix) * '-')
259 ostr.write(self.colorize(header, self.BLUE, True))
262 for section_name, names in stats.items():
263 for stat_name, stat_nick in names.items():
264 sub_header += self.UNDERLINE_SEQ \
266 stat_nick.ljust(self.col_width(stat_nick)),
269 sub_header = sub_header[0:-1] + self.colorize('|', self.BLUE)
271 ostr.write(sub_header)
273 def _print_vals(self, ostr, dump, last_dump):
275 Print a single row of values to `ostr`, based on deltas between `dump` and
279 fit, changed = self.get_stats_that_fit()
281 self._print_headers(ostr)
282 for section_name, names in fit.items():
283 for stat_name, stat_nick in names.items():
284 stat_type = self._schema[section_name][stat_name]['type']
285 if bool(stat_type & COUNTER):
286 n = max(dump[section_name][stat_name] -
287 last_dump[section_name][stat_name], 0)
288 elif bool(stat_type & LONG_RUNNING_AVG):
289 entries = dump[section_name][stat_name]['avgcount'] - \
290 last_dump[section_name][stat_name]['avgcount']
292 n = (dump[section_name][stat_name]['sum'] -
293 last_dump[section_name][stat_name]['sum']) \
295 n *= 1000.0 # Present in milliseconds
299 n = dump[section_name][stat_name]
301 val_row += self.format_dimless(n, self.col_width(stat_nick))
303 val_row = val_row[0:-1]
304 val_row += self.colorize("|", self.BLUE)
305 val_row = val_row[0:-len(self.colorize("|", self.BLUE))]
306 ostr.write("{0}\n".format(val_row))
308 def _should_include(self, sect, name, prio):
310 boolean: should we output this stat?
312 1) If self._statpats exists and the name filename-glob-matches
313 anything in the list, and prio is high enough, or
314 2) If self._statpats doesn't exist and prio is high enough
319 sectname = '.'.join((sect, name))
321 p for p in self._statpats
322 if fnmatch(name, p) or fnmatch(sectname, p)
326 if self._min_prio is not None and prio is not None:
327 return (prio >= self._min_prio)
331 def _load_schema(self):
333 Populate our instance-local copy of the daemon's performance counter
334 schema, and work out which stats we will display.
336 self._schema = json.loads(
337 admin_socket(self.asok_path, ["perf", "schema"]).decode('utf-8'),
338 object_pairs_hook=OrderedDict)
340 # Build list of which stats we will display
341 self._stats = OrderedDict()
342 for section_name, section_stats in self._schema.items():
343 for name, schema_data in section_stats.items():
344 prio = schema_data.get('priority', 0)
345 if self._should_include(section_name, name, prio):
346 if section_name not in self._stats:
347 self._stats[section_name] = OrderedDict()
348 self._stats[section_name][name] = schema_data['nick']
349 if not len(self._stats):
350 raise RuntimeError("no stats selected by filters")
352 def _handle_sigwinch(self, signo, frame):
353 self.termsize.update()
355 def run(self, interval, count=None, ostr=sys.stdout):
357 Print output at regular intervals until interrupted.
359 :param ostr: Stream to which to send output
363 self._colored = self.supports_color(ostr)
365 self._print_headers(ostr)
367 last_dump = json.loads(admin_socket(self.asok_path, ["perf", "dump"]).decode('utf-8'))
368 rows_since_header = 0
371 signal(SIGWINCH, self._handle_sigwinch)
373 dump = json.loads(admin_socket(self.asok_path, ["perf", "dump"]).decode('utf-8'))
374 if rows_since_header >= self.termsize.rows - 2:
375 self._print_headers(ostr)
376 rows_since_header = 0
377 self._print_vals(ostr, dump, last_dump)
378 if count is not None:
382 rows_since_header += 1
385 # time.sleep() is interrupted by SIGWINCH; avoid that
386 end = time.time() + interval
387 while time.time() < end:
388 time.sleep(end - time.time())
390 except KeyboardInterrupt:
393 def list(self, ostr=sys.stdout):
395 Show all selected stats with section, full name, nick, and prio
397 table = PrettyTable(('section', 'name', 'nick', 'prio'))
398 table.align['section'] = 'l'
399 table.align['name'] = 'l'
400 table.align['nick'] = 'l'
401 table.align['prio'] = 'r'
403 for section_name, section_stats in self._stats.items():
404 for name, nick in section_stats.items():
405 prio = self._schema[section_name][name].get('priority') or 0
406 table.add_row((section_name, name, nick, prio))
407 ostr.write(table.get_string(hrules=HEADER) + '\n')