blob: 2c7332e7e328a75ce5fb3f2d3eab966451c04c1f [file] [log] [blame]
#!/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()