blob: 52981b9b5594f65d509a2d15d969eaa374e3a95a [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
BEGINS = []
ENDS = []
SUPPRESSES = []
CRASH_DIR = '/tmp/fuchsia_crash'
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',
'RED': '\033[1;31m',
'GREEN': '\033[1;32m',
'YELLOW': '\033[1:33m',
'BLUE': '\033[1:34m',
'MAGENTA': '\033[1:35m',
'CYAN': '\033[1:36m',
}
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 = 'out/build-zircon/tools/loglistener'
fuchsia_dir = os.environ['FUCHSIA_DIR']
return '{}/{} {}'.format(fuchsia_dir, 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
return COLORS[color_name.upper()] + string + RESET
@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 not BEGINS or any(keyword in line for keyword in BEGINS):
if not is_suppressed.is_in_session:
print '\n' * 3
is_suppressed.is_in_session = True
if any(keyword in line for keyword in ENDS):
is_suppressed.is_in_session = False
if not is_suppressed.is_in_session:
return True
if any(keyword in line for keyword in SUPPRESSES):
return True
return False
def colorize(line_incoming):
"""Color-code the log line.
Args:
line_incoming: log line.
Returns:
color-coded log line
"""
# TODO (porce): Case-insensitive search
# Treat "warn", "WARN", "wArN" in the same way.
line = line_incoming
if any(k in line for k in COLOR_LINES.keys()):
for k, v in COLOR_LINES.iteritems():
if k not in line:
continue
line = color(line, v)
break
if any(k in line for k in COLOR_WORDS.keys()):
rep = {}
for k, v in COLOR_WORDS.iteritems():
rep[k] = color(k, v)
rep = dict((re.escape(k), v) for k, v in rep.iteritems())
pattern = re.compile('|'.join(rep.keys()))
line = pattern.sub(lambda m: rep[re.escape(m.group(0))], 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():
fuchsia_dir = os.environ['FUCHSIA_DIR']
zircon_build_dir = os.environ['ZIRCON_BUILD_DIR']
fuchsia_build_dir = os.environ['FUCHSIA_BUILD_DIR']
decode_cmd = '{}/zircon/scripts/symbolize --build-dir {} {}'.format(
fuchsia_dir, zircon_build_dir, fuchsia_build_dir)
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:
Dictonary 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 seperated 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('--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()
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:
global CRASH_DIR
CRASH_DIR = args.crashdir
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()