Merge pull request #1005 from adamcik/fix/808

Add per logger colors
This commit is contained in:
Stein Magnus Jodal 2015-02-25 23:18:08 +01:00
commit 5536c79be5
7 changed files with 67 additions and 40 deletions

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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'})