| #!/usr/bin/env python | |
| # Copyright 2017 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """flog is a developer friendly log listener with automatic crash symbol decoder. | |
| The crash is archived to /tmp/fuchsia-crash or a specified directory. | |
| Configurable keywords or lines with them can be color coded. | |
| """ | |
| import argparse | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| from time import gmtime | |
| from time import strftime | |
| CASE_SENSITIVE = False | |
| BEGINS = [] | |
| ENDS = [] | |
| SUPPRESSES = [] | |
| CRASH_DIR = '/tmp/fuchsia_crash' | |
| # TODO(porce): Support regular expression | |
| # TODO(porce): Support compatibility with REGEX_ regex expressions | |
| COLOR_LINES = {'error': 'red'} | |
| COLOR_WORDS = { | |
| 'warn': 'white-on-red', | |
| 'error': 'white-on-red', | |
| 'fail': 'white-on-red', | |
| 'exception': 'white-on-red', | |
| 'address': 'green', | |
| 'did not add device in bind': 'green', | |
| } | |
| RESET = '\033[1;0m' | |
| COLORS = { | |
| 'WHITE-ON-RED': '\033[41;37m', | |
| 'BLACK': '\033[30;1m', | |
| 'RED': '\033[31;1m', | |
| 'GREEN': '\033[32;1m', | |
| 'YELLOW': '\033[33;1m', | |
| 'BLUE': '\033[34;1m', | |
| 'MAGENTA': '\033[35;1m', | |
| 'CYAN': '\033[36m;1m', | |
| } | |
| FIRST_LOG_AFTER_BOOTUP = '[00000.000] 00000.00000> bootdata:' | |
| # REGEX_* are constructed when the command line arguments are parsed. | |
| REGEX_BEGINS = '' | |
| REGEX_ENDS = '' | |
| REGEX_SUPPRESS = '' | |
| REGEX_COLOR_LINES = '' | |
| REGEX_COLOR_WORDS = '' | |
| def static_vars(**kwargs): | |
| def decorate(func): | |
| for k in kwargs: | |
| setattr(func, k, kwargs[k]) | |
| return func | |
| return decorate | |
| def now_str(): | |
| return strftime('%Y%m%d_%H%M%S', gmtime()) | |
| def get_log_listener(args): | |
| # return 'cat /tmp/z' # Unit test | |
| listener = 'fx log' | |
| return '{} {}'.format(listener, ' '.join(x for x in args)) | |
| @static_vars(crash_dump=[]) | |
| @static_vars(is_crashing=False) | |
| def monitor_crash(line): | |
| if '<==' in line and 'exception' in line: | |
| monitor_crash.is_crashing = True | |
| if monitor_crash.is_crashing is True: | |
| monitor_crash.crash_dump.append(line) | |
| if ': end' in line and 'bt#' in line: | |
| decode_backtrace(monitor_crash.crash_dump, CRASH_DIR) | |
| monitor_crash.crash_dump = [] | |
| monitor_crash.is_crashing = False | |
| def color(string, color_name): | |
| if color_name.upper() not in COLORS: | |
| return string | |
| ends_in_newline = string.endswith('\n') | |
| if ends_in_newline: | |
| string = string[:-1] | |
| result = '{}{}{}'.format(COLORS[color_name.upper()], string, RESET) | |
| if ends_in_newline: | |
| result += '\n' | |
| return result | |
| def anymatch(test_string, regexes): | |
| global CASE_SENSITIVE | |
| if CASE_SENSITIVE: | |
| ans = re.search(regexes, test_string) | |
| else: | |
| ans = re.search(regexes, test_string, flags=re.IGNORECASE) | |
| return ans.group(0) if ans else None | |
| @static_vars(is_in_session=False) | |
| def is_suppressed(line): | |
| """Test if the log may be printed or not. | |
| Args: | |
| line (str): A log line. | |
| Returns: | |
| True if the log line should be suppressed. False otherwise. | |
| """ | |
| if line.startswith(FIRST_LOG_AFTER_BOOTUP): | |
| # A reboot occurs. Reset to default. | |
| is_suppressed.is_in_session = False | |
| if not BEGINS or anymatch(line, REGEX_BEGINS): | |
| if not is_suppressed.is_in_session: | |
| print '\n' * 3 | |
| is_suppressed.is_in_session = True | |
| if anymatch(line, REGEX_ENDS): | |
| is_suppressed.is_in_session = False | |
| if not is_suppressed.is_in_session: | |
| return True | |
| if anymatch(line, REGEX_SUPPRESSES): | |
| return True | |
| return False | |
| def colorize(line_incoming): | |
| """Color-code the log line. | |
| Args: | |
| line_incoming: log line. | |
| Returns: | |
| color-coded log line | |
| """ | |
| line = line_incoming | |
| k = anymatch(line, REGEX_COLOR_LINES) | |
| if k: | |
| v = COLOR_LINES.get(k.lower(), '') | |
| line = color(line, v) | |
| return line | |
| if anymatch(line, REGEX_COLOR_WORDS): | |
| # TODO(porce): Maybe there are less costly ways. | |
| for k, v in COLOR_WORDS.iteritems(): | |
| replacement = r'%s\1%s' % (COLORS[v.upper()], RESET) | |
| search = '(?i)(' + '|'.join(map(re.escape, [k])) + ')' | |
| line = re.sub(search, replacement, line) | |
| return line | |
| def print_log(line): | |
| if is_suppressed(line): | |
| return | |
| print colorize(line), | |
| def hijack_stdout(cmd): | |
| proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) | |
| return iter(proc.stdout.readline, '') | |
| def get_decode_cmd(): | |
| decode_cmd = 'fx symbolize' | |
| return decode_cmd | |
| def print_bt(line): | |
| """Print backtrace with some colors. | |
| Args: | |
| line (str): Backtrace line. | |
| """ | |
| tokens = line.split(' at ') | |
| methods = tokens[:-1] | |
| methods.append('') | |
| methods_text = ' at '.join(x for x in methods) | |
| path = tokens[-1] | |
| path_tokens = path.split('/') | |
| paths = path_tokens[:-1] | |
| paths.append('') | |
| path_text = '/'.join(x for x in paths) | |
| path_colored = path_text + color(path_tokens[-1], 'red') | |
| print methods_text, path_colored, | |
| def decode_backtrace(crash_dump, dst_dir): | |
| """A wrapper to fsymbolize. | |
| Args: | |
| crash_dump (array): array of log lines. | |
| dst_dir (str): directory to archive to. | |
| """ | |
| os.system('mkdir -p {}'.format(dst_dir)) | |
| crash_file_path = '{}/{}.crash'.format(dst_dir, now_str()) | |
| tmp_file = '{}/tmp.dump'.format(dst_dir) | |
| f = open(tmp_file, 'w') | |
| for line in crash_dump: | |
| f.write(line) | |
| f.close() | |
| cmd = '{} < {} > {}'.format(get_decode_cmd(), tmp_file, crash_file_path) | |
| os.system(cmd) | |
| print '\n\n' | |
| is_start = False | |
| with open(crash_file_path, 'r') as f: | |
| for line in f: | |
| if 'start of symbolized stack:' in line: | |
| is_start = True | |
| if is_start: | |
| print_bt(line) | |
| print '\n\n' | |
| def parse_color_map(string): | |
| """Converted comma separated text into a color code map. | |
| Args: | |
| string (str): a text line | |
| Returns: | |
| Dictionary whose key is a text pattern and value is the color name. | |
| """ | |
| m = {} | |
| items = string.split(',') | |
| for item in items: | |
| sep = ':' | |
| if sep not in item: | |
| continue | |
| idx = item.rfind(sep) | |
| text = item[:idx] | |
| color_name = item[idx + 1:] | |
| if text.__len__() == 0: | |
| continue | |
| m[text] = color_name | |
| return m | |
| def proc_cmdline(): | |
| """Argument parser. | |
| Returns: | |
| args. | |
| """ | |
| example_commands = """ | |
| Pro tip: Use comma separated texts for multiple matches | |
| Example: | |
| $ flog --begin \'my module starts,rare event\' | |
| --end \'my module ends\' | |
| --suppress \'verbose,chatty\' | |
| --lines \'error msg:red,warn:blue\' | |
| --words \'register 0x00:green,exit:yellow\' | |
| """ | |
| p = argparse.ArgumentParser( | |
| description='A friendly Fuchsia log listener', | |
| epilog=example_commands, | |
| formatter_class=argparse.RawDescriptionHelpFormatter) | |
| p.add_argument('--begin', type=str, help='trigger texts to start logging') | |
| p.add_argument('--end', type=str, help='trigger texts to end logging') | |
| p.add_argument( | |
| '--case', type=bool, help='match case-sensitively', default=False) | |
| p.add_argument('--suppress', type=str, help='text to suppress the line') | |
| p.add_argument('--lines', type=str, help='colorize the line. {text:color}') | |
| p.add_argument('--words', type=str, help='colorize the word. {text:color}') | |
| p.add_argument('--crashdir', type=str, help='directory to store crash files.') | |
| p.add_argument( | |
| 'remainders', | |
| nargs=argparse.REMAINDER, | |
| help='arguments passed to loglistener') | |
| args = p.parse_args() | |
| global CASE_SENSITIVE | |
| global BEGINS | |
| global ENDS | |
| global SUPPRESSES | |
| global COLOR_LINES | |
| global COLOR_WORDS | |
| global CRASH_DIR | |
| CASE_SENSITIVE = args.case | |
| if args.begin: | |
| BEGINS.extend(args.begin.split(',')) | |
| if args.end: | |
| ENDS.extend(args.end.split(',')) | |
| if args.suppress: | |
| SUPPRESSES.extend(args.suppress.split(',')) | |
| if args.lines: | |
| COLOR_LINES.update(parse_color_map(args.lines)) | |
| if args.words: | |
| COLOR_WORDS.update(parse_color_map(args.words)) | |
| if args.crashdir: | |
| CRASH_DIR = args.crashdir | |
| global REGEX_BEGINS | |
| global REGEX_ENDS | |
| global REGEX_SUPPRESSES | |
| global REGEX_COLOR_LINES | |
| global REGEX_COLOR_WORDS | |
| # TODO(porce): Support regex input | |
| REGEX_BEGINS = '|'.join(BEGINS) | |
| REGEX_ENDS = '|'.join(ENDS) | |
| REGEX_SUPPRESSES = '|'.join(SUPPRESSES) | |
| REGEX_COLOR_LINES = '|'.join(k for k, v in COLOR_LINES.iteritems()) | |
| REGEX_COLOR_WORDS = '|'.join(k for k, v in COLOR_WORDS.iteritems()) | |
| return args | |
| def main(): | |
| args = proc_cmdline() | |
| cmd = get_log_listener(args.remainders) | |
| for line in hijack_stdout(cmd): | |
| print_log(line) | |
| monitor_crash(line) | |
| main() |