From 47b44791a6ef3c8c181bf8fefaf3cd3fec59f0d9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Jul 2014 00:20:34 +0200 Subject: [PATCH] log: Colorize logs, unless logging/color is false Fixes #772 --- docs/changelog.rst | 5 ++- docs/config.rst | 5 +++ mopidy/config/__init__.py | 1 + mopidy/config/default.conf | 1 + mopidy/utils/log.py | 79 +++++++++++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 157f0d73..14c476b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,10 +31,13 @@ Feature release. release, like Mopidy 0.18, or migrate the configuration to the new format by hand. -**Configuration** +**Logging** - Fix proper decoding of exception messages that depends on the user's locale. +- Colorize logs depending on log level. This can be turned off with the new + :confval:`logging/color` configuration. (Fixes: :issue:`772`) + **Extension support** - Removed the :class:`~mopidy.ext.Extension` methods that were deprecated in diff --git a/docs/config.rst b/docs/config.rst index 692204d9..ffef2ffe 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -123,6 +123,11 @@ Audio configuration Logging configuration --------------------- +.. confval:: logging/color + + Whether or not to colorize the console log based on log level. Defaults to + ``true``. + .. confval:: logging/console_format The log format used for informational logging. diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 1f9f5e2d..ecec1f89 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -15,6 +15,7 @@ from mopidy.utils import path, versioning logger = logging.getLogger(__name__) _logging_schema = ConfigSchema('logging') +_logging_schema['color'] = Boolean() _logging_schema['console_format'] = String() _logging_schema['debug_format'] = String() _logging_schema['debug_file'] = Path() diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index 839c983d..37f69ce1 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -1,4 +1,5 @@ [logging] +color = true console_format = %(levelname)-8s %(message)s debug_format = %(levelname)-8s %(asctime)s [%(process)d:%(threadName)s] %(name)s\n %(message)s debug_file = mopidy.log diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 837ef17f..5d6d3635 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging import logging.config import logging.handlers +import platform LOG_LEVELS = { @@ -73,7 +74,10 @@ def setup_console_logging(config, verbosity_level): log_format = config['logging']['debug_format'] formatter = logging.Formatter(log_format) - handler = logging.StreamHandler() + if config['logging']['color']: + handler = ColorizingStreamHandler() + else: + handler = logging.StreamHandler() handler.addFilter(verbosity_filter) handler.setFormatter(formatter) @@ -104,3 +108,76 @@ class VerbosityFilter(logging.Filter): else: required_log_level = LOG_LEVELS[self.verbosity_level]['root'] return record.levelno >= required_log_level + + +class ColorizingStreamHandler(logging.StreamHandler): + """ + Stream handler which colorizes the log using ANSI escape sequences. + + Does nothing on Windows, which doesn't support ANSI escape sequences. + + This implementation is based upon https://gist.github.com/vsajip/758430, + which is: + + Copyright (C) 2010-2012 Vinay Sajip. All rights reserved. + Licensed under the new BSD license. + """ + + color_map = { + 'black': 0, + 'red': 1, + 'green': 2, + 'yellow': 3, + 'blue': 4, + 'magenta': 5, + 'cyan': 6, + 'white': 7, + } + + # Map logging levels to (background, foreground, bold/intense) + level_map = { + logging.DEBUG: (None, 'blue', False), + logging.INFO: (None, 'white', False), + logging.WARNING: (None, 'yellow', False), + logging.ERROR: (None, 'red', False), + logging.CRITICAL: ('red', 'white', True), + } + csi = '\x1b[' + reset = '\x1b[0m' + + is_windows = platform.system() == 'Windows' + + @property + def is_tty(self): + isatty = getattr(self.stream, 'isatty', None) + return isatty and isatty() + + def emit(self, record): + try: + message = self.format(record) + self.stream.write(message) + self.stream.write(getattr(self, 'terminator', '\n')) + self.flush() + except Exception: + self.handleError(record) + + def format(self, record): + message = logging.StreamHandler.format(self, record) + if not self.is_tty or self.is_windows: + return message + return self.colorize(message, record) + + def colorize(self, message, record): + if record.levelno in self.level_map: + bg, fg, bold = self.level_map[record.levelno] + params = [] + if bg in self.color_map: + params.append(str(self.color_map[bg] + 40)) + if fg in self.color_map: + params.append(str(self.color_map[fg] + 30)) + if bold: + params.append('1') + if params: + message = ''.join(( + self.csi, ';'.join(params), 'm', message, self.reset)) + return message