qutebrowser/qutebrowser/utils/log.py

516 lines
17 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2016-01-04 07:12:39 +01:00
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-05-23 16:11:55 +02:00
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Loggers and utilities related to logging."""
import os
import sys
2014-08-05 23:23:15 +02:00
import html as pyhtml
import logging
2014-08-26 19:10:14 +02:00
import contextlib
import collections
2014-11-09 22:59:14 +01:00
import faulthandler
2015-02-24 07:12:25 +01:00
import traceback
import warnings
2014-05-23 16:11:55 +02:00
from PyQt5 import QtCore
# Optional imports
2014-05-25 20:00:48 +02:00
try:
2014-08-25 22:26:52 +02:00
import colorama
2014-05-25 20:00:48 +02:00
except ImportError:
2014-08-25 22:26:52 +02:00
colorama = None
try:
2014-08-26 19:10:14 +02:00
import colorlog
except ImportError:
2014-08-26 19:10:14 +02:00
colorlog = None
2014-08-25 22:26:52 +02:00
else:
2014-09-02 20:44:58 +02:00
# WORKAROUND
2014-08-25 22:26:52 +02:00
# colorlog calls colorama.init() which we don't want, also it breaks our
2014-08-26 06:35:58 +02:00
# sys.stdout/sys.stderr if they are None. Bugreports:
# https://code.google.com/p/colorama/issues/detail?id=61
# https://github.com/borntyping/python-colorlog/issues/13
2014-09-03 22:20:17 +02:00
# This should be (partially) fixed in colorama 0.3.2.
# (stream only gets wrapped if it's not None)
2014-08-25 22:26:52 +02:00
if colorama is not None:
colorama.deinit()
2014-05-25 20:00:48 +02:00
2014-06-06 12:29:33 +02:00
# Log formats to use.
2016-02-12 11:02:08 +01:00
SIMPLE_FMT = '{asctime:8} {levelname}: {message}'
2014-06-06 12:29:33 +02:00
EXTENDED_FMT = ('{asctime:8} {levelname:8} {name:10} {module}:{funcName}:'
'{lineno} {message}')
2016-02-17 07:04:25 +01:00
SIMPLE_FMT_COLORED = ('%(green)s%(asctime)-8s%(reset)s '
'%(log_color)s%(levelname)s%(reset)s: %(message)s')
2014-08-05 23:35:20 +02:00
EXTENDED_FMT_COLORED = (
'%(green)s%(asctime)-8s%(reset)s '
'%(log_color)s%(levelname)-8s%(reset)s '
'%(cyan)s%(name)-10s %(module)s:%(funcName)s:%(lineno)s%(reset)s '
'%(log_color)s%(message)s%(reset)s'
)
EXTENDED_FMT_HTML = (
'<tr>'
'<td><pre>%(green)s%(asctime)-8s%(reset)s</pre></td>'
'<td><pre>%(log_color)s%(levelname)-8s%(reset)s</pre></td>'
'<td></pre>%(cyan)s%(name)-10s</pre></td>'
'<td><pre>%(cyan)s%(module)s:%(funcName)s:%(lineno)s%(reset)s</pre></td>'
'<td><pre>%(log_color)s%(message)s%(reset)s</pre></td>'
'</tr>'
)
2014-06-06 12:29:33 +02:00
DATEFMT = '%H:%M:%S'
2014-06-25 10:00:27 +02:00
LOG_COLORS = {
2014-08-29 07:33:37 +02:00
'VDEBUG': 'white',
'DEBUG': 'white',
2014-06-25 10:00:27 +02:00
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
2014-06-06 12:29:33 +02:00
2014-08-29 07:33:37 +02:00
# We first monkey-patch logging to support our VDEBUG level before getting the
# loggers. Based on http://stackoverflow.com/a/13638084
VDEBUG_LEVEL = 9
logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG')
logging.VDEBUG = VDEBUG_LEVEL
2015-04-13 08:52:58 +02:00
def vdebug(self, msg, *args, **kwargs):
2014-08-29 07:33:37 +02:00
"""Log with a VDEBUG level.
VDEBUG is used when a debug message is rather verbose, and probably of
little use to the end user or for post-mortem debugging, i.e. the content
probably won't change unless the code changes.
"""
if self.isEnabledFor(VDEBUG_LEVEL):
# pylint: disable=protected-access
2015-04-13 08:52:58 +02:00
self._log(VDEBUG_LEVEL, msg, args, **kwargs)
2014-05-23 16:11:55 +02:00
2014-08-29 07:33:37 +02:00
logging.Logger.vdebug = vdebug
# The different loggers used.
2014-08-26 19:10:14 +02:00
statusbar = logging.getLogger('statusbar')
completion = logging.getLogger('completion')
destroy = logging.getLogger('destroy')
modes = logging.getLogger('modes')
webview = logging.getLogger('webview')
mouse = logging.getLogger('mouse')
misc = logging.getLogger('misc')
url = logging.getLogger('url')
procs = logging.getLogger('procs')
commands = logging.getLogger('commands')
init = logging.getLogger('init')
signals = logging.getLogger('signals')
hints = logging.getLogger('hints')
keyboard = logging.getLogger('keyboard')
downloads = logging.getLogger('downloads')
js = logging.getLogger('js') # Javascript console messages
qt = logging.getLogger('qt') # Warnings produced by Qt
rfc6266 = logging.getLogger('rfc6266')
2014-10-13 21:38:28 +02:00
ipc = logging.getLogger('ipc')
2014-11-04 20:06:58 +01:00
shlexer = logging.getLogger('shlexer')
save = logging.getLogger('save')
2015-04-13 08:49:04 +02:00
message = logging.getLogger('message')
config = logging.getLogger('config')
2015-04-13 08:52:58 +02:00
sessions = logging.getLogger('sessions')
ram_handler = None
class CriticalQtWarning(Exception):
"""Exception raised when there's a critical Qt warning."""
def init_log(args):
"""Init loggers based on the argparse namespace passed."""
level = args.loglevel.upper()
try:
numeric_level = getattr(logging, level)
except AttributeError:
raise ValueError("Invalid log level: {}".format(args.loglevel))
2014-06-06 12:29:33 +02:00
if numeric_level > logging.DEBUG and args.debug:
numeric_level = logging.DEBUG
console, ram = _init_handlers(numeric_level, args.color, args.force_color,
args.loglines)
2014-08-26 19:10:14 +02:00
root = logging.getLogger()
if console is not None:
2014-08-25 06:58:41 +02:00
if args.logfilter is not None:
console.addFilter(LogFilter(args.logfilter.split(',')))
root.addHandler(console)
if ram is not None:
root.addHandler(ram)
2014-06-06 12:29:33 +02:00
root.setLevel(logging.NOTSET)
logging.captureWarnings(True)
warnings.simplefilter('default')
QtCore.qInstallMessageHandler(qt_message_handler)
2014-06-06 12:29:33 +02:00
2014-08-26 19:10:14 +02:00
@contextlib.contextmanager
def disable_qt_msghandler():
2014-09-22 20:44:07 +02:00
"""Contextmanager which temporarily disables the Qt message handler."""
old_handler = QtCore.qInstallMessageHandler(None)
try:
yield
finally:
QtCore.qInstallMessageHandler(old_handler)
def _init_handlers(level, color, force_color, ram_capacity):
2014-06-06 12:29:33 +02:00
"""Init log handlers.
Args:
level: The numeric logging level.
color: Whether to use color if available.
force_color: Force colored output.
2014-06-06 12:29:33 +02:00
"""
global ram_handler
2014-06-25 10:00:27 +02:00
console_fmt, ram_fmt, html_fmt, use_colorama = _init_formatters(
level, color, force_color)
2014-06-06 12:29:33 +02:00
if sys.stderr is None:
console_handler = None
2014-06-06 12:29:33 +02:00
else:
strip = False if force_color else None
if use_colorama:
stream = colorama.AnsiToWin32(sys.stderr, strip=strip)
else:
stream = sys.stderr
console_handler = logging.StreamHandler(stream)
console_handler.setLevel(level)
2014-06-25 10:00:27 +02:00
console_handler.setFormatter(console_fmt)
2014-06-06 12:29:33 +02:00
if ram_capacity == 0:
ram_handler = None
else:
ram_handler = RAMHandler(capacity=ram_capacity)
ram_handler.setLevel(logging.NOTSET)
2014-06-25 10:00:27 +02:00
ram_handler.setFormatter(ram_fmt)
ram_handler.html_formatter = html_fmt
2014-06-06 12:29:33 +02:00
return console_handler, ram_handler
def _init_formatters(level, color, force_color):
2014-06-06 12:29:33 +02:00
"""Init log formatters.
Args:
level: The numeric logging level.
color: Whether to use color if available.
force_color: Force colored output.
2014-06-06 12:29:33 +02:00
Return:
A (console_formatter, ram_formatter, use_colorama) tuple.
console_formatter/ram_formatter: logging.Formatter instances.
use_colorama: Whether to use colorama.
"""
if level <= logging.DEBUG:
console_fmt = EXTENDED_FMT
console_fmt_colored = EXTENDED_FMT_COLORED
2014-05-25 20:00:48 +02:00
else:
2014-06-06 12:29:33 +02:00
console_fmt = SIMPLE_FMT
console_fmt_colored = SIMPLE_FMT_COLORED
ram_formatter = logging.Formatter(EXTENDED_FMT, DATEFMT, '{')
2014-06-25 10:00:27 +02:00
html_formatter = HTMLFormatter(EXTENDED_FMT_HTML, DATEFMT,
log_colors=LOG_COLORS)
if sys.stderr is None:
2014-06-25 10:00:27 +02:00
return None, ram_formatter, html_formatter, False
2016-04-30 18:38:20 +02:00
2014-06-06 12:29:33 +02:00
use_colorama = False
2016-04-30 18:38:20 +02:00
color_supported = colorlog is not None and (os.name == 'posix' or colorama)
if color_supported and (sys.stderr.isatty() or force_color) and color:
2014-08-26 19:10:14 +02:00
console_formatter = colorlog.ColoredFormatter(
console_fmt_colored, DATEFMT, log_colors=LOG_COLORS)
2016-04-30 16:59:59 +02:00
if colorama and os.name != 'posix':
2014-06-06 12:29:33 +02:00
use_colorama = True
else:
2014-06-06 12:29:33 +02:00
console_formatter = logging.Formatter(console_fmt, DATEFMT, '{')
2014-06-25 10:00:27 +02:00
return console_formatter, ram_formatter, html_formatter, use_colorama
2014-06-02 22:43:32 +02:00
def qt_message_handler(msg_type, context, msg):
"""Qt message handler to redirect qWarning etc. to the logging system.
Args:
QtMsgType msg_type: The level of the message.
QMessageLogContext context: The source code location of the message.
msg: The message text.
"""
# Mapping from Qt logging levels to the matching logging module levels.
# Note we map critical to ERROR as it's actually "just" an error, and fatal
# to critical.
2014-06-03 14:34:38 +02:00
qt_to_logging = {
QtCore.QtDebugMsg: logging.DEBUG,
QtCore.QtWarningMsg: logging.WARNING,
QtCore.QtCriticalMsg: logging.ERROR,
QtCore.QtFatalMsg: logging.CRITICAL,
2014-06-02 22:43:32 +02:00
}
try:
# pylint: disable=no-member,useless-suppression
qt_to_logging[QtCore.QtInfoMsg] = logging.INFO
except AttributeError:
# Qt < 5.5
pass
2015-09-09 22:53:37 +02:00
# Change levels of some well-known messages to debug so they don't get
# shown to the user.
2015-09-09 22:53:37 +02:00
#
# If a message starts with any text in suppressed_msgs, it's not logged as
# error.
suppressed_msgs = [
# PNGs in Qt with broken color profile
2015-06-12 16:59:33 +02:00
# https://bugreports.qt.io/browse/QTBUG-39788
2015-09-09 22:53:37 +02:00
'libpng warning: iCCP: Not recognizing known sRGB profile that has '
'been edited', # flake8: disable=E131
'libpng warning: iCCP: known incorrect sRGB profile',
# Hopefully harmless warning
2015-09-09 22:53:37 +02:00
'OpenType support missing for script ',
# Error if a QNetworkReply gets two different errors set. Harmless Qt
# bug on some pages.
2015-06-12 16:59:33 +02:00
# https://bugreports.qt.io/browse/QTBUG-30298
2015-09-09 22:53:37 +02:00
'QNetworkReplyImplPrivate::error: Internal problem, this method must '
'only be called once.',
2014-11-16 16:25:37 +01:00
# Sometimes indicates missing text, but most of the time harmless
2015-09-09 22:53:37 +02:00
'load glyph failed ',
2015-06-12 16:59:33 +02:00
# Harmless, see https://bugreports.qt.io/browse/QTBUG-42479
2015-09-09 22:53:37 +02:00
'content-type missing in HTTP POST, defaulting to '
'application/x-www-form-urlencoded. '
'Use QNetworkRequest::setHeader() to fix this problem.',
2015-06-12 16:59:33 +02:00
# https://bugreports.qt.io/browse/QTBUG-43118
2015-09-09 22:53:37 +02:00
'Using blocking call!',
2015-01-07 06:59:48 +01:00
# Hopefully harmless
'"Method "GetAll" with signature "s" on interface '
2015-09-09 22:53:37 +02:00
'"org.freedesktop.DBus.Properties" doesn\'t exist',
'"Method \\"GetAll\\" with signature \\"s\\" on interface '
'\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"',
2015-05-31 00:11:33 +02:00
'WOFF support requires QtWebKit to be built with zlib support.',
# Weird Enlightment/GTK X extensions
'QXcbWindow: Unhandled client message: "_E_',
'QXcbWindow: Unhandled client message: "_ECORE_',
'QXcbWindow: Unhandled client message: "_GTK_',
# Happens on AppVeyor CI
'SetProcessDpiAwareness failed:',
# https://bugreports.qt.io/browse/QTBUG-49174
'QObject::connect: Cannot connect (null)::stateChanged('
'QNetworkSession::State) to '
'QNetworkReplyHttpImpl::_q_networkSessionStateChanged('
'QNetworkSession::State)',
]
if sys.platform == 'darwin':
suppressed_msgs += [
2015-09-09 22:53:37 +02:00
'libpng warning: iCCP: known incorrect sRGB profile',
# https://bugreports.qt.io/browse/QTBUG-47154
'virtual void QSslSocketBackendPrivate::transmit() SSLRead failed '
'with: -9805', # flake8: disable=E131
]
# Messages which will trigger an exception immediately
critical_msgs = [
'Could not parse stylesheet of object',
]
if any(msg.strip().startswith(pattern) for pattern in critical_msgs):
# For some reason, the stack gets lost when raising here...
logger = logging.getLogger('misc')
logger.error("Got critical Qt warning!", stack_info=True)
raise CriticalQtWarning(msg)
elif any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
level = logging.DEBUG
else:
level = qt_to_logging[msg_type]
2015-11-09 19:55:05 +01:00
if context.function is None:
func = 'none'
2015-11-09 19:55:05 +01:00
elif ':' in context.function:
func = '"{}"'.format(context.function)
else:
func = context.function
2015-11-09 19:55:05 +01:00
if context.category is None or context.category == 'default':
name = 'qt'
else:
name = 'qt-' + context.category
if msg.splitlines()[0] == ('This application failed to start because it '
'could not find or load the Qt platform plugin '
'"xcb".'):
# Handle this message specially.
msg += ("\n\nOn Archlinux, this should fix the problem:\n"
" pacman -S libxkbcommon-x11")
2014-11-09 22:59:14 +01:00
faulthandler.disable()
stack = ''.join(traceback.format_stack())
record = qt.makeRecord(name, level, context.file, context.line, msg, None,
2015-02-24 07:12:25 +01:00
None, func, sinfo=stack)
2014-06-02 22:43:32 +02:00
qt.handle(record)
2014-06-02 22:19:35 +02:00
@contextlib.contextmanager
def hide_qt_warning(pattern, logger='qt'):
"""Hide Qt warnings matching the given regex."""
log_filter = QtWarningFilter(pattern)
logger_obj = logging.getLogger(logger)
logger_obj.addFilter(log_filter)
try:
yield
finally:
logger_obj.removeFilter(log_filter)
class QtWarningFilter(logging.Filter):
"""Filter to filter Qt warnings.
Attributes:
_pattern: The start of the message.
"""
def __init__(self, pattern):
super().__init__()
self._pattern = pattern
def filter(self, record):
"""Determine if the specified record is to be logged."""
2015-11-13 22:27:41 +01:00
do_log = not record.msg.strip().startswith(self._pattern)
return do_log
class LogFilter(logging.Filter):
"""Filter to filter log records based on the commandline argument.
The default Filter only supports one name to show - we support a
comma-separated list instead.
Attributes:
_names: A list of names that should be logged.
"""
def __init__(self, names):
super().__init__()
self._names = names
def filter(self, record):
"""Determine if the specified record is to be logged."""
if self._names is None:
return True
2014-06-06 16:51:24 +02:00
if record.levelno > logging.DEBUG:
# More important than DEBUG, so we won't filter at all
2014-06-06 15:48:42 +02:00
return True
for name in self._names:
if record.name == name:
return True
elif not record.name.startswith(name):
continue
elif record.name[len(name)] == '.':
return True
return False
class RAMHandler(logging.Handler):
"""Logging handler which keeps the messages in a deque in RAM.
2015-06-07 01:11:16 +02:00
Loosely based on logging.BufferingHandler which is unsuitable because it
uses a simple list rather than a deque.
Attributes:
_data: A deque containing the logging records.
"""
def __init__(self, capacity):
super().__init__()
2014-06-25 10:00:27 +02:00
self.html_formatter = None
if capacity != -1:
self._data = collections.deque(maxlen=capacity)
else:
self._data = collections.deque()
def emit(self, record):
2014-08-29 07:33:37 +02:00
if record.levelno >= logging.DEBUG:
# We don't log VDEBUG to RAM.
self._data.append(record)
2014-06-25 10:00:27 +02:00
def dump_log(self, html=False):
"""Dump the complete formatted log data as as string.
FIXME: We should do all the HTML formatter via jinja2.
2014-10-01 22:23:27 +02:00
(probably obsolete when moving to a widget for logging,
https://github.com/The-Compiler/qutebrowser/issues/34
"""
lines = []
fmt = self.html_formatter.format if html else self.format
self.acquire()
try:
records = list(self._data)
finally:
self.release()
for record in records:
2014-06-25 10:00:27 +02:00
lines.append(fmt(record))
return '\n'.join(lines)
2014-06-25 10:00:27 +02:00
class HTMLFormatter(logging.Formatter):
2014-11-19 22:46:52 +01:00
"""Formatter for HTML-colored log messages, similar to colorlog.
2014-09-24 22:22:02 +02:00
Attributes:
_log_colors: The colors to use for logging levels.
_colordict: The colordict passed to the logger.
"""
2014-06-25 10:00:27 +02:00
def __init__(self, fmt, datefmt, log_colors):
"""Constructor.
Args:
fmt: The format string to use.
datefmt: The date format to use.
log_colors: The colors to use for logging levels.
"""
super().__init__(fmt, datefmt)
self._log_colors = log_colors
self._colordict = {}
# We could solve this nicer by using CSS, but for this simple case this
# works.
for color in ['black', 'red', 'green', 'yellow', 'blue', 'purple',
'cyan', 'white']:
self._colordict[color] = '<font color="{}">'.format(color)
self._colordict['reset'] = '</font>'
def format(self, record):
record.__dict__.update(self._colordict)
if record.levelname in self._log_colors:
color = self._log_colors[record.levelname]
record.log_color = self._colordict[color]
else:
record.log_color = ''
2014-06-25 21:11:03 +02:00
for field in ['msg', 'filename', 'funcName', 'levelname', 'module',
'name', 'pathname', 'processName', 'threadName']:
data = str(getattr(record, field))
2014-08-05 23:23:15 +02:00
setattr(record, field, pyhtml.escape(data))
2015-04-13 08:52:58 +02:00
msg = super().format(record)
if not msg.endswith(self._colordict['reset']):
msg += self._colordict['reset']
return msg
2014-06-25 21:11:03 +02:00
def formatTime(self, record, datefmt=None):
out = super().formatTime(record, datefmt)
2014-08-05 23:23:15 +02:00
return pyhtml.escape(out)