#!/usr/bin/python3 # vim: et ts=4 ai sw=4 sts=0 import sys import json from optparse import OptionParser import os import glob import gzip from datetime import datetime, date, timedelta import dateutil.parser as dateparser import configparser from ansible.config.manager import find_ini_config_file from ansible.utils.color import hostcolor, stringc from ansible import constants as C from collections import Counter if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty(): HAS_COLOR = False else: HAS_COLOR = True logpath = '/var/log/ansible' default_search_terms = ['CHANGED', 'FAILED'] date_terms = { "today": lambda: datetime.today().replace(hour=0, minute=0, second=0, microsecond=0), "yesterday": lambda: datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(1), } def colorByCat(category, txt=None): if not txt: txt = category if 'OK' in category: color_out = stringc(txt, C.COLOR_OK) elif "FAILED" in category: color_out = stringc(txt, C.COLOR_ERROR) elif "CHANGED" in category: color_out = stringc(txt, C.COLOR_CHANGED) elif "SKIPPED" in category: color_out = stringc(txt, C.COLOR_SKIP) elif "UNREACHABLE" in category: color_out = stringc(txt, C.COLOR_UNREACHABLE) else: # This hack make sure the text width is the same as any other colored text color_out = u'\x1b[0;00m%s\x1b[0m' % (txt,) if not HAS_COLOR: color_out = txt return color_out def colorByStats(txt, stats): if stats['failures'] != 0: return stringc(txt, C.COLOR_ERROR) elif stats['unreachable'] != 0: return stringc(txt, C.COLOR_UNREACHABLE) elif stats['changed'] != 0: return stringc(txt, C.COLOR_CHANGED) else: return stringc(txt, C.COLOR_OK) def colorByCount(txt, count, color): s = "%s%s" % (txt, count) if count > 0 and HAS_COLOR: s = stringc(s, color) return s def parse_info(infofile): data = {} with open(infofile) as f: content = f.read() obj_list = [x+'}' for x in content.split('\n}')] plays = [] for obj in obj_list[:-1]: js = json.loads(obj) if 'play' in js: plays.append(js) else: data.update(json.loads(obj)) data['plays'] = plays return data def format_stats(stats): return "%s %s %s %s" % ( colorByCount("ok:", stats['ok'], C.COLOR_OK), colorByCount("chg:", stats['changed'], C.COLOR_CHANGED), colorByCount("unr:", stats['unreachable'], C.COLOR_UNREACHABLE), colorByCount("fail:", stats['failures'], C.COLOR_ERROR)) def col_width(rows): widths = [] for col in zip(*(rows)): col_width = max(map(len,col)) widths.append(col_width) widths[-1] = 0 # don't pad last column return widths def date_cheat(datestr): dc = date_terms.get(datestr, lambda: dateparser.parse(datestr)) return dc() def parse_args(args): usage = """ logview [options] [-d datestr] [-p playbook] examples: logview -d yesterday -l # lists playbooks run on that date logview -s OK -s FAILED -d yesterday # list events from yesterday that failed or were ok logview -s CHANGED -d yesterday -p mirrorlist # list events that changed from the mirrorlist playbook logview -s ANY -d yesterday -p mirrorlist # list all events from the mirrorlist playbook """ parser = OptionParser(usage=usage) parser.add_option("-d", default='today', dest='datestr', help="time string of when you want logs") parser.add_option("-p", default='*', dest='playbook', help="the playbook you want to look for") parser.add_option("-H", default=[], dest='hostname', action='append', help="Limit to the specified hostname") parser.add_option("-m", default=False, dest='message', action='store_true', help='Show tasks output') parser.add_option("-v", default=False, dest='verbose', action='store_true', help='Verbose') parser.add_option("-s", default=[], dest='search_terms', action='append', help="status to search for") parser.add_option("-l", default=False, dest="list_pb", action='store_true', help="list playbooks for a specific date") parser.add_option("-L", default=False, dest="list_all_pb", action='store_true', help="list all playbooks ever ran") parser.add_option("--profile", default=False, dest="profile", action='store_true', help="output timing input per task") (opts, args) = parser.parse_args(args) opts.datestr = date_cheat(opts.datestr) if not opts.search_terms: opts.search_terms = default_search_terms opts.search_terms = list(map(str.upper, opts.search_terms)) return opts, args def search_logs(opts, logfiles): rows = [] headers = [] msg = '' for fn in sorted(logfiles): hostname = os.path.basename(fn).replace('.log', '').replace('.gz', '') timestamp = os.path.basename(os.path.dirname(fn)) if opts.hostname and hostname not in opts.hostname: continue try: with gzip.open(fn) as f: f.read() open_f = gzip.open(fn, "rt") except: open_f = open(fn) for line in open_f: things = line.split('\t') if len(things) < 5: print("(logview error - unhandled line): %r\n" % line) continue # See callback_plugins/logdetail.py for how these lines get created. # MSG_FORMAT="%(now)s\t%(count)s\t%(category)s\t%(name)s\t%(data)s\n" task_ts, count, category, name, data = things if category in opts.search_terms or 'ANY' in opts.search_terms: dur = None last_col = "" slurp = json.loads(data) if opts.profile: st = slurp.get('task_start', 0) end = slurp.get('task_end', 0) if st and end: dur = '%.2fs' % (float(end) - float(st)) state = colorByCat(category) c_hostname = colorByCat(category, hostname) if "STATS" in category: if type(slurp) == dict: name = format_stats(slurp) c_hostname = colorByStats(hostname, slurp) state = colorByStats(category, slurp) result = [timestamp, c_hostname, task_ts, count, state] if not name: name = slurp.get("task_module") result.append(name) if dur: last_col += "%s " % (dur,) if not opts.verbose: if type(slurp) == dict: for term in ['cmd',]: if term in slurp: last_col += '\t%s:%s' % (term, slurp.get(term, None)) if opts.message: for term in ['msg', 'stdout']: if term in slurp: value = slurp.get(term, None) if type(value) is list: value = "\n".join(value) if value: last_col += '\n%s: %s\n' % (term, colorByCat(category, value.strip())) else: last_col += '\n' last_col += json.dumps(slurp, indent=4) last_col += '\n' result.append(last_col) rows.append(result) return rows def main(args): cfg = find_ini_config_file() if cfg: cp = configparser.ConfigParser() cp.read(cfg) logpath = cp.get('callback_logdetail', "log_path", fallback="/var/log/ansible") opts, args = parse_args(args) rows = [] # List play summary if opts.list_pb or opts.list_all_pb: rows.append([ "Date", colorByCat("", "Playbook"), "Ran By", "Hosts", "Stats"]) for r,d,f in os.walk(logpath): for file in f: if file.endswith('.info'): pb = parse_info(os.path.join(r,file)) pb_name = os.path.splitext(os.path.basename(pb['playbook']))[0] pb_date = datetime.fromtimestamp(pb['playbook_start']) if ( opts.list_all_pb or ( opts.datestr != opts.datestr.replace(hour=0, minute=0, second=0, microsecond=0) and opts.datestr == pb_date) or ( opts.datestr == opts.datestr.replace(hour=0, minute=0, second=0, microsecond=0) and opts.datestr.date() == pb_date.date())): stats = Counter() hosts = [] if "stats" in pb: for host, stat in pb['stats'].items(): del stat['task_userid'] stats += Counter(stat) hosts.append(host) host_count = len(set(hosts)) pb_name = colorByStats(pb_name, stats) summary = format_stats(stats) # summary = "ok:%s chd:%s unr:%s faild:%s" % (stats['ok'], stats['changed'], stats['unreachable'], stats['failures']) rows.append([ str(pb_date), pb_name, pb['userid'], str(host_count), summary ]) m_widths = col_width(rows) if len(rows) <= 1: print("no log") else: for row in rows: print(" ".join((val.ljust(width) for val, width in zip(row, m_widths))).strip()) # Play detail else: for pb in glob.glob(os.path.join(logpath, opts.playbook)): pb_name = os.path.basename(pb) for pb_logdir in glob.glob(os.path.join(pb, opts.datestr.strftime("%Y/%m/%d"))): if opts.datestr != opts.datestr.replace(hour=0, minute=0, second=0, microsecond=0): logfiles = glob.glob(pb_logdir + '/' + opts.datestr.strftime("%H.%M.%S") + '/*.log*') else: logfiles = glob.glob(pb_logdir + '/*/*.log*') rows = search_logs(opts, logfiles) if rows: m_widths = col_width(rows) print(pb_name) for row in rows: print(" ".join((val.ljust(width) for val, width in zip(row, m_widths)))) #print(pb_name) #print(msg) if __name__ == "__main__": sys.exit(main(sys.argv[1:]))