diff --git a/docs/changelog.rst b/docs/changelog.rst index e1082b09..7b3e97b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -53,6 +53,8 @@ v0.20.0 (UNRELEASED) - Add custom log level ``TRACE`` (numerical level 5), which can be used by Mopidy and extensions to log at an even more detailed level than ``DEBUG``. +- Add support for per logger color overrides. (Fixes: :issue:`808`) + **Local backend** - Add cover URL to all scanned files with MusicBrainz album IDs. (Fixes: diff --git a/docs/config.rst b/docs/config.rst index 03bb83ac..69945ab8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -131,6 +131,14 @@ Logging configuration level to use for that logger, one of ``debug``, ``info``, ``warning``, ``error``, or ``critical``. +.. confval:: logcolors/* + + The ``logcolors`` config section can be used to change the log color for + specific parts of Mopidy during development or debugging. Each key in the + config section should match the name of a logger. The value is the color + to use for that logger, one of ``black``, ``red``, ``green``, ``yellow``, + ``blue``, ``magenta``, ``cyan`` or ``white``. + .. _the Python logging docs: http://docs.python.org/2/library/logging.config.html diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 885ea3a6..7c4f8755 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -22,7 +22,8 @@ _logging_schema['debug_format'] = String() _logging_schema['debug_file'] = Path() _logging_schema['config_file'] = Path(optional=True) -_loglevels_schema = LogLevelConfigSchema('loglevels') +_loglevels_schema = MapConfigSchema('loglevels', LogLevel()) +_logcolors_schema = MapConfigSchema('logcolors', LogColor()) _audio_schema = ConfigSchema('audio') _audio_schema['mixer'] = String() @@ -42,7 +43,8 @@ _proxy_schema['password'] = Secret(optional=True) # NOTE: if multiple outputs ever comes something like LogLevelConfigSchema # _outputs_schema = config.AudioOutputConfigSchema() -_schemas = [_logging_schema, _loglevels_schema, _audio_schema, _proxy_schema] +_schemas = [_logging_schema, _loglevels_schema, _logcolors_schema, + _audio_schema, _proxy_schema] _INITIAL_HELP = """ # For further information about options in this file see: diff --git a/mopidy/config/schemas.py b/mopidy/config/schemas.py index 56826a53..2b055663 100644 --- a/mopidy/config/schemas.py +++ b/mopidy/config/schemas.py @@ -94,17 +94,16 @@ class ConfigSchema(collections.OrderedDict): return result -class LogLevelConfigSchema(object): - """Special cased schema for handling a config section with loglevels. +class MapConfigSchema(object): + """Schema for handling multiple unknown keys with the same type. - Expects the config keys to be logger names and the values to be log levels - as understood by the :class:`LogLevel` config value. Does not sub-class - :class:`ConfigSchema`, but implements the same serialize/deserialize - interface. + Does not sub-class :class:`ConfigSchema`, but implements the same + serialize/deserialize interface. """ - def __init__(self, name): + + def __init__(self, name, value_type): self.name = name - self._config_value = types.LogLevel() + self._value_type = value_type def deserialize(self, values): errors = {} @@ -112,7 +111,7 @@ class LogLevelConfigSchema(object): for key, value in values.items(): try: - result[key] = self._config_value.deserialize(value) + result[key] = self._value_type.deserialize(value) except ValueError as e: # deserialization failed result[key] = None errors[key] = str(e) @@ -121,5 +120,5 @@ class LogLevelConfigSchema(object): def serialize(self, values, display=False): result = collections.OrderedDict() for key in sorted(values.keys()): - result[key] = self._config_value.serialize(values[key], display) + result[key] = self._value_type.serialize(values[key], display) return result diff --git a/mopidy/config/types.py b/mopidy/config/types.py index 785ec55a..d074458b 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -6,7 +6,7 @@ import socket from mopidy import compat from mopidy.config import validators -from mopidy.utils import path +from mopidy.utils import log, path def decode(value): @@ -197,6 +197,17 @@ class List(ConfigValue): return b'\n ' + b'\n '.join(encode(v) for v in value if v) +class LogColor(ConfigValue): + def deserialize(self, value): + validators.validate_choice(value.lower(), log.COLORS) + return value.lower() + + def serialize(self, value, display=False): + if value.lower() in log.COLORS: + return value.lower() + return b'' + + class LogLevel(ConfigValue): """Log level value. diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 3c7ee599..6343a866 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -82,7 +82,7 @@ def setup_console_logging(config, verbosity_level): formatter = logging.Formatter(log_format) if config['logging']['color']: - handler = ColorizingStreamHandler() + handler = ColorizingStreamHandler(config.get('logcolors', {})) else: handler = logging.StreamHandler() handler.addFilter(verbosity_filter) @@ -117,6 +117,11 @@ class VerbosityFilter(logging.Filter): return record.levelno >= required_log_level +#: Available log colors. +COLORS = [b'black', b'red', b'green', b'yellow', b'blue', b'magenta', b'cyan', + b'white'] + + class ColorizingStreamHandler(logging.StreamHandler): """ Stream handler which colorizes the log using ANSI escape sequences. @@ -130,17 +135,6 @@ class ColorizingStreamHandler(logging.StreamHandler): 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 = { TRACE_LOG_LEVEL: (None, 'blue', False), @@ -150,11 +144,18 @@ class ColorizingStreamHandler(logging.StreamHandler): logging.ERROR: (None, 'red', False), logging.CRITICAL: ('red', 'white', True), } + # Map logger name to foreground colors + logger_map = {} + csi = '\x1b[' reset = '\x1b[0m' is_windows = platform.system() == 'Windows' + def __init__(self, logger_colors): + super(ColorizingStreamHandler, self).__init__() + self.logger_map = logger_colors + @property def is_tty(self): isatty = getattr(self.stream, 'isatty', None) @@ -173,19 +174,23 @@ class ColorizingStreamHandler(logging.StreamHandler): 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): + for name, color in self.logger_map.iteritems(): + if record.name.startswith(name): + return self.colorize(message, fg=color) 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 self.colorize(message, bg=bg, fg=fg, bold=bold) + return message + + def colorize(self, message, bg=None, fg=None, bold=False): + params = [] + if bg in COLORS: + params.append(str(COLORS.index(bg) + 40)) + if fg in COLORS: + params.append(str(COLORS.index(fg) + 30)) + if bold: + params.append('1') + if params: + message = ''.join(( + self.csi, ';'.join(params), 'm', message, self.reset)) return message diff --git a/tests/config/test_schemas.py b/tests/config/test_schemas.py index 8412b899..502bf61c 100644 --- a/tests/config/test_schemas.py +++ b/tests/config/test_schemas.py @@ -86,9 +86,9 @@ class ConfigSchemaTest(unittest.TestCase): self.assertNotIn('foo', errors) -class LogLevelConfigSchemaTest(unittest.TestCase): +class MapConfigSchemaTest(unittest.TestCase): def test_conversion(self): - schema = schemas.LogLevelConfigSchema('test') + schema = schemas.MapConfigSchema('test', types.LogLevel()) result, errors = schema.deserialize( {'foo.bar': 'DEBUG', 'baz': 'INFO'})