log: implement JSON as logging output

Fixes #1501

Enabled via the --json-logging parameter.
This commit is contained in:
Daniel Schadt 2016-05-25 00:12:51 +02:00
parent 9a0fa9068c
commit 4a59a1f112
4 changed files with 62 additions and 51 deletions

View File

@ -86,6 +86,8 @@ def get_argparser():
default=2000, type=int)
debug.add_argument('--debug', help="Turn on debugging options.",
action='store_true')
debug.add_argument('--json-logging', action='store_true', help="Output log"
" lines in JSON format (one object per line).")
debug.add_argument('--nocolor', help="Turn off colored logging.",
action='store_false', dest='color')
debug.add_argument('--force-color', help="Force colored logging",

View File

@ -28,6 +28,7 @@ import collections
import faulthandler
import traceback
import warnings
import json
from PyQt5 import QtCore
# Optional imports
@ -142,7 +143,7 @@ def init_log(args):
numeric_level = logging.DEBUG
console, ram = _init_handlers(numeric_level, args.color, args.force_color,
args.loglines)
args.json_logging, args.loglines)
root = logging.getLogger()
if console is not None:
if args.logfilter is not None:
@ -166,17 +167,18 @@ def disable_qt_msghandler():
QtCore.qInstallMessageHandler(old_handler)
def _init_handlers(level, color, force_color, ram_capacity):
def _init_handlers(level, color, force_color, json_logging, ram_capacity):
"""Init log handlers.
Args:
level: The numeric logging level.
color: Whether to use color if available.
force_color: Force colored output.
json_logging: Output log lines in JSON (this disables all colors).
"""
global ram_handler
console_fmt, ram_fmt, html_fmt, use_colorama = _init_formatters(
level, color, force_color)
level, color, force_color, json_logging)
if sys.stderr is None:
console_handler = None
@ -201,13 +203,14 @@ def _init_handlers(level, color, force_color, ram_capacity):
return console_handler, ram_handler
def _init_formatters(level, color, force_color):
def _init_formatters(level, color, force_color, json_logging):
"""Init log formatters.
Args:
level: The numeric logging level.
color: Whether to use color if available.
force_color: Force colored output.
json_logging: Format lines as JSON (disables all color).
Return:
A (console_formatter, ram_formatter, use_colorama) tuple.
@ -222,6 +225,10 @@ def _init_formatters(level, color, force_color):
if sys.stderr is None:
return None, ram_formatter, html_formatter, False
if json_logging:
console_formatter = JSONFormatter()
return console_formatter, ram_formatter, html_formatter, False
use_colorama = False
color_supported = os.name == 'posix' or colorama
@ -538,3 +545,16 @@ class HTMLFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
out = super().formatTime(record, datefmt)
return pyhtml.escape(out)
class JSONFormatter(logging.Formatter):
"""Formatter for JSON-encoded log messages."""
def format(self, record):
obj = {}
for field in ['created', 'levelname', 'name', 'module', 'funcName',
'lineno', 'levelno']:
obj[field] = getattr(record, field)
obj['message'] = record.getMessage()
return json.dumps(obj)

View File

@ -29,6 +29,7 @@ import logging
import tempfile
import contextlib
import itertools
import json
import yaml
import pytest
@ -61,62 +62,53 @@ class LogLine(testprocess.Line):
"""A parsed line from the qutebrowser log output.
Attributes:
timestamp/loglevel/category/module/function/line/message:
timestamp/loglevel/category/module/function/line/message/levelname:
Parsed from the log output.
expected: Whether the message was expected or not.
"""
LOG_RE = re.compile(r"""
(?P<timestamp>\d\d:\d\d:\d\d)
\ (?P<loglevel>VDEBUG|DEBUG|INFO|WARNING|ERROR)
\ +(?P<category>[\w.]+)
\ +(?P<module>(\w+|Unknown\ module)):
(?P<function>[^"][^:]*|"[^"]+"):
(?P<line>\d+)
\ (?P<message>.+)
""", re.VERBOSE)
def __init__(self, data):
super().__init__(data)
match = self.LOG_RE.match(data)
if match is None:
try:
line = json.loads(data)
except json.decoder.JSONDecodeError:
raise testprocess.InvalidLine(data)
self.timestamp = datetime.datetime.strptime(match.group('timestamp'),
'%H:%M:%S')
loglevel = match.group('loglevel')
if loglevel == 'VDEBUG':
self.loglevel = log.VDEBUG_LEVEL
else:
self.loglevel = getattr(logging, loglevel)
self.category = match.group('category')
module = match.group('module')
if module == 'Unknown module':
self.module = None
else:
self.module = module
function = match.group('function')
if function == 'none':
self.function = None
else:
self.function = function.strip('"')
line = int(match.group('line'))
if self.function is None and line == 0:
self.line = None
else:
self.line = line
self.timestamp = datetime.datetime.fromtimestamp(line['created'])
self.loglevel = line['levelno']
self.levelname = line['levelname']
self.category = line['name']
self.module = line['module']
self.function = line['funcName']
self.line = line['lineno']
self.full_message = line['message']
msg_match = re.match(r'^(\[(?P<prefix>\d+s ago)\] )?(?P<message>.*)',
match.group('message'))
self.full_message)
self.prefix = msg_match.group('prefix')
self.message = msg_match.group('message')
self.expected = is_ignored_qt_message(self.message)
def formatted_str(self):
"""Return a formatted colorized line.strip()
This returns a line like qute without --json-logging would produce.
"""
format_dict = {
'asctime': self.timestamp.strftime(log.DATEFMT),
'log_color': log.LOG_COLORS[self.levelname],
'levelname': self.levelname,
'reset': log.ColoredFormatter.RESET_ESCAPE,
'name': self.category,
'module': self.module,
'funcName': self.function,
'lineno': self.line,
'message': self.full_message,
}
format_dict.update(log.ColoredFormatter.COLOR_ESCAPES)
return log.EXTENDED_FMT.format_map(format_dict)
class QuteProc(testprocess.Process):
@ -169,11 +161,6 @@ class QuteProc(testprocess.Process):
self.ready.emit()
def _parse_line(self, line):
# http://stackoverflow.com/a/14693789/2085149
colored_line = line
ansi_escape = re.compile(r'\x1b[^m]*m')
line = ansi_escape.sub('', line)
try:
log_line = LogLine(line)
except testprocess.InvalidLine:
@ -187,6 +174,7 @@ class QuteProc(testprocess.Process):
else:
raise
colored_line = log_line.formatted_str()
self._log(colored_line)
start_okay_message_load = (
@ -243,7 +231,7 @@ class QuteProc(testprocess.Process):
def _default_args(self):
return ['--debug', '--no-err-windows', '--temp-basedir',
'--force-color', 'about:blank']
'--json-logging', 'about:blank']
def path_to_url(self, path, *, port=None, https=False):
"""Get a URL based on a filename for the localhost webserver.

View File

@ -85,6 +85,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
quteproc_process.after_test(did_fail=False)
@pytest.mark.skip
@pytest.mark.parametrize('data, attrs', [
(
# Normal message