@@ -35,6 +35,10 @@ import subprocess
from collections import defaultdict, namedtuple
from functools import reduce
from datetime import datetime
+import glob
+import string
+import logging
+from logging.handlers import RotatingFileHandler
VMX_EXIT_REASONS = {
'EXCEPTION_NMI': 0,
@@ -974,6 +978,8 @@ MAX_REGEX_LEN = 44
SORT_DEFAULT = 0
MIN_DELAY = 0.1
MAX_DELAY = 25.5
+SIZE_DEFAULT = '10M'
+LOGCOUNT_DEFAULT = 6
class Tui(object):
@@ -1535,6 +1541,64 @@ def log(stats, opts, frmt, keys):
break
+def rotating_log(stats, opts, frmt, keys):
+ """Prints statistics to file in csv format."""
+ def init(opts, frmt):
+ # Regular RotatingFileHandler doesn't add a header to each file,
+ # so we create our own version
+ class MyRotatingFileHandler(RotatingFileHandler):
+ def __init__(self, logfile):
+ super(MyRotatingFileHandler,
+ self).__init__(logfile, mode='w', maxBytes=opts.size_num,
+ backupCount=LOGCOUNT_DEFAULT-1)
+ self._header = ""
+ self._log = None
+
+ def doRollover(self):
+ super(MyRotatingFileHandler, self).doRollover()
+ if self._log is not None and self._header != "":
+ self._log.debug(self._header)
+
+ def setHeader(self, header, log):
+ self._header = header
+ self._log = log
+ if not self.stream or self.stream.tell() == 0:
+ self._log.debug(self._header)
+
+ # Regular Formatter would prepend a timestamp to the header,
+ # so we create our own version again
+ class MyFormatter(logging.Formatter):
+ def __init__(self, fmt, datefmt):
+ logging.Formatter.__init__(self, fmt=fmt, datefmt=datefmt)
+
+ def format(self, record):
+ if record.levelno == logging.DEBUG:
+ return record.getMessage()
+ return logging.Formatter.format(self, record)
+ try:
+ hdl = MyRotatingFileHandler(opts.rotating_log)
+ except:
+ sys.exit("Error setting up csv log with file '%s'"
+ % opts.rotating_log)
+ formatter = MyFormatter('%(asctime)s%(message)s', '%Y-%m-%d %H:%M:%S')
+ hdl.setFormatter(formatter)
+
+ logger = logging.getLogger('MyLogger')
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(hdl)
+ hdl.setHeader(frmt.get_banner(), logger)
+
+ return logger
+
+ log = init(opts, frmt)
+ while True:
+ try:
+ time.sleep(opts.set_delay)
+ log.info(frmt.get_statline(keys, stats.get()))
+ except KeyboardInterrupt:
+ break
+
+
def is_delay_valid(delay):
"""Verify delay is in valid value range."""
msg = None
@@ -1580,6 +1644,26 @@ Interactive Commands:
Press any other key to refresh statistics immediately.
""" % (PATH_DEBUGFS_KVM, PATH_DEBUGFS_TRACING)
+ def convert_from_si(opts):
+ try:
+ factor = 1000000
+ num = int(opts.size.rstrip(string.ascii_letters))
+ unit = opts.size.lstrip(string.digits)
+ except ValueError:
+ sys.exit("Error: Invalid argument to -S/--size: '%s'" % opts.size)
+ if num <= 0:
+ sys.exit("Error: Argument to -S/--size must be >0")
+ if unit != '':
+ if unit in ['m', 'M']:
+ factor = 1000000
+ elif unit in ['g', 'G']:
+ factor = 1000000000
+ elif unit in ['t', 'T']:
+ factor = 1000000000000
+ else:
+ sys.exit("Error: Unsupported unit suffix '%s'" % unit)
+ opts.size_num = int(num * factor / LOGCOUNT_DEFAULT)
+
class Guest_to_pid(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
try:
@@ -1606,7 +1690,8 @@ Press any other key to refresh statistics immediately.
argparser.add_argument('-c', '--csv',
action='store_true',
default=False,
- help='log in csv format - requires option -l/--log',
+ help='log in csv format - requires option -l/--log '
+ 'or -r/--rotating-log'
)
argparser.add_argument('-d', '--debugfs',
action='store_true',
@@ -1639,6 +1724,12 @@ Press any other key to refresh statistics immediately.
default=0,
help='restrict statistics to pid',
)
+ argparser.add_argument('-r', '--rotating-log',
+ type=str,
+ default='',
+ metavar='FILE',
+ help='write a rotating log to FILE'
+ )
argparser.add_argument('-s', '--set-delay',
type=float,
default=DELAY_DEFAULT,
@@ -1646,14 +1737,28 @@ Press any other key to refresh statistics immediately.
help='set delay between refreshs (value range: '
'%s-%s secs)' % (MIN_DELAY, MAX_DELAY),
)
+ argparser.add_argument('-S', '--size',
+ type=str,
+ default='',
+ help='''maximum total file size
+supported suffixes: MGT (Megabytes (default), Gigabytes, Terabytes)
+default: %s''' % SIZE_DEFAULT,
+ )
argparser.add_argument('-t', '--tracepoints',
action='store_true',
default=False,
help='retrieve statistics from tracepoints',
)
options = argparser.parse_args()
- if options.csv and not options.log:
- sys.exit('Error: Option -c/--csv requires -l/--log')
+ if options.csv and not options.log and not options.rotating_log:
+ sys.exit('Error: Option -c/--csv requires one of -l/--log or '
+ '-r/--rotating-log')
+ if options.rotating_log:
+ if options.log:
+ sys.exit('Error: Cannot mix -l/--log and -r/--rotating-log')
+ if not options.size:
+ options.size = SIZE_DEFAULT
+ convert_from_si(options)
try:
# verify that we were passed a valid regex up front
re.compile(options.fields)
@@ -1733,13 +1838,17 @@ def main():
sys.stdout.write(' ' + '\n '.join(sorted(set(event_list))) + '\n')
sys.exit(0)
- if options.log:
+ if options.log or options.rotating_log:
keys = sorted(stats.get().keys())
if options.csv:
frmt = CSVFormat(keys)
else:
frmt = StdFormat(keys)
- log(stats, options, frmt, keys)
+
+ if options.log:
+ log(stats, options, frmt, keys)
+ else:
+ rotating_log(stats, options, frmt, keys)
elif not options.once:
with Tui(stats, options) as tui:
tui.show_stats()
@@ -66,7 +66,7 @@ OPTIONS
-c::
--csv=<file>::
- log in csv format - requires option -l/--log
+ log in csv format - requires option -l/--log or -r/--rotating-log
-d::
--debugfs::
@@ -96,10 +96,21 @@ OPTIONS
--pid=<pid>::
limit statistics to one virtual machine (pid)
+-r<file>::
+--rotating-log=<file>::
+ log output to rotating logfiles prefixed <file> - also
+ see option -S/--size
+
-s::
--set-delay::
set delay between refreshs (value range: 0.1-25.5 secs)
+-S<size>::
+--size=<size>::
+ maximum total file size for option -r/--rotating-log.
+ Supported suffixes: MGT (Megabytes (default), Gigabytes, Terabytes).
+ Default: 10M
+
-t::
--tracepoints::
retrieve statistics from tracepoints