From 6e226326fd7a4f2cf619c5fcfab16abe522631b8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 11 Jul 2014 23:55:12 +0200 Subject: [PATCH 01/10] http: Explicitly define template path for this router Fixes #774 --- mopidy/http/handlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/http/handlers.py b/mopidy/http/handlers.py index 18b56de2..048c9ddf 100644 --- a/mopidy/http/handlers.py +++ b/mopidy/http/handlers.py @@ -152,6 +152,9 @@ class ClientListHandler(tornado.web.RequestHandler): self.apps = apps self.statics = statics + def get_template_path(self): + return os.path.dirname(__file__) + def get(self): set_mopidy_headers(self) From 7a08cb69c1637f7ca1c16266d7ff7b63cd095a20 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 13 Jul 2014 23:43:28 +0200 Subject: [PATCH 02/10] log: Reorder module --- mopidy/utils/log.py | 52 ++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index cde07693..837ef17f 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -5,6 +5,15 @@ import logging.config import logging.handlers +LOG_LEVELS = { + -1: dict(root=logging.ERROR, mopidy=logging.WARNING), + 0: dict(root=logging.ERROR, mopidy=logging.INFO), + 1: dict(root=logging.WARNING, mopidy=logging.DEBUG), + 2: dict(root=logging.INFO, mopidy=logging.DEBUG), + 3: dict(root=logging.DEBUG, mopidy=logging.DEBUG), +} + + class DelayedHandler(logging.Handler): def __init__(self): logging.Handler.__init__(self) @@ -46,32 +55,6 @@ def setup_logging(config, verbosity_level, save_debug_log): _delayed_handler.release() -LOG_LEVELS = { - -1: dict(root=logging.ERROR, mopidy=logging.WARNING), - 0: dict(root=logging.ERROR, mopidy=logging.INFO), - 1: dict(root=logging.WARNING, mopidy=logging.DEBUG), - 2: dict(root=logging.INFO, mopidy=logging.DEBUG), - 3: dict(root=logging.DEBUG, mopidy=logging.DEBUG), -} - - -class VerbosityFilter(logging.Filter): - def __init__(self, verbosity_level, loglevels): - self.verbosity_level = verbosity_level - self.loglevels = loglevels - - def filter(self, record): - for name, required_log_level in self.loglevels.items(): - if record.name == name or record.name.startswith(name + '.'): - return record.levelno >= required_log_level - - if record.name.startswith('mopidy'): - required_log_level = LOG_LEVELS[self.verbosity_level]['mopidy'] - else: - required_log_level = LOG_LEVELS[self.verbosity_level]['root'] - return record.levelno >= required_log_level - - def setup_console_logging(config, verbosity_level): if verbosity_level < min(LOG_LEVELS.keys()): verbosity_level = min(LOG_LEVELS.keys()) @@ -104,3 +87,20 @@ def setup_debug_logging_to_file(config): handler.setFormatter(formatter) logging.getLogger('').addHandler(handler) + + +class VerbosityFilter(logging.Filter): + def __init__(self, verbosity_level, loglevels): + self.verbosity_level = verbosity_level + self.loglevels = loglevels + + def filter(self, record): + for name, required_log_level in self.loglevels.items(): + if record.name == name or record.name.startswith(name + '.'): + return record.levelno >= required_log_level + + if record.name.startswith('mopidy'): + required_log_level = LOG_LEVELS[self.verbosity_level]['mopidy'] + else: + required_log_level = LOG_LEVELS[self.verbosity_level]['root'] + return record.levelno >= required_log_level From 47b44791a6ef3c8c181bf8fefaf3cd3fec59f0d9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Jul 2014 00:20:34 +0200 Subject: [PATCH 03/10] 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 From dda06dd8de5f01b6713f161b4af43a2714187acc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 15 Jul 2014 00:33:30 +0200 Subject: [PATCH 04/10] config: Add optional support to Boolean type --- docs/changelog.rst | 4 ++++ mopidy/config/types.py | 6 ++++++ tests/config/test_types.py | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 14c476b0..c8ce1841 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,10 @@ Feature release. release, like Mopidy 0.18, or migrate the configuration to the new format by hand. +**Configuration** + +- Add ``optional=True`` support to :class:`mopidy.config.Boolean`. + **Logging** - Fix proper decoding of exception messages that depends on the user's locale. diff --git a/mopidy/config/types.py b/mopidy/config/types.py index a6736371..4498cb67 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -151,7 +151,13 @@ class Boolean(ConfigValue): true_values = ('1', 'yes', 'true', 'on') false_values = ('0', 'no', 'false', 'off') + def __init__(self, optional=False): + self._required = not optional + def deserialize(self, value): + validators.validate_required(value, self._required) + if not value: + return None if value.lower() in self.true_values: return True elif value.lower() in self.false_values: diff --git a/tests/config/test_types.py b/tests/config/test_types.py index e4a6f22e..dfb439be 100644 --- a/tests/config/test_types.py +++ b/tests/config/test_types.py @@ -214,6 +214,10 @@ class BooleanTest(unittest.TestCase): self.assertEqual(b'false', result) self.assertIsInstance(result, bytes) + def test_deserialize_respects_optional(self): + value = types.Boolean(optional=True) + self.assertEqual(None, value.deserialize('')) + # TODO: test None or other invalid values into serialize? From 2177550649e31708dafb0889fbe690163b064c78 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 15 Jul 2014 01:36:42 +0200 Subject: [PATCH 05/10] http: Prettify HTTP router list in debug log --- mopidy/http/actor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/http/actor.py b/mopidy/http/actor.py index 484fc946..96c71c8c 100644 --- a/mopidy/http/actor.py +++ b/mopidy/http/actor.py @@ -14,6 +14,7 @@ import tornado.websocket from mopidy import models, zeroconf from mopidy.core import CoreListener from mopidy.http import handlers +from mopidy.utils import formatting logger = logging.getLogger(__name__) @@ -88,7 +89,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): logger.debug( 'HTTP routes from extensions: %s', - list((l[0], l[1]) for l in request_handlers)) + formatting.indent('\n'.join( + '%r: %r' % (r[0], r[1]) for r in request_handlers))) return request_handlers def _get_app_request_handlers(self): From 7e489ccb0b6216e147da3a3f5206dbb8df5bd104 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 22:15:45 +0200 Subject: [PATCH 06/10] exc: Test ScannerError (cherry picked from commit 8f59dd69adca2e6c1595159e5350bf5a4e69988f) --- tests/test_exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index da8fed90..3452a06b 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -15,3 +15,7 @@ class ExceptionsTest(unittest.TestCase): def test_extension_error_is_a_mopidy_exception(self): self.assert_(issubclass( exceptions.ExtensionError, exceptions.MopidyException)) + + def test_scanner_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.ScannerError, exceptions.MopidyException)) From 44664f27966a0f5422a4701067ca9a2c8ee5c7d1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 22:17:46 +0200 Subject: [PATCH 07/10] exc: Add {Backend,Frontend,Mixer}Error exceptions (cherry picked from commit bf8307f329ce03b7a311d62aa50fdd7833048f32) --- mopidy/exceptions.py | 12 ++++++++++++ tests/test_exceptions.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/mopidy/exceptions.py b/mopidy/exceptions.py index 025d8fad..bf9b6dd9 100644 --- a/mopidy/exceptions.py +++ b/mopidy/exceptions.py @@ -16,9 +16,21 @@ class MopidyException(Exception): self._message = message +class BackendError(MopidyException): + pass + + class ExtensionError(MopidyException): pass +class FrontendError(MopidyException): + pass + + +class MixerError(MopidyException): + pass + + class ScannerError(MopidyException): pass diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 3452a06b..47b3080d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -12,10 +12,22 @@ class ExceptionsTest(unittest.TestCase): self.assertEqual(exc.message, 'foo') self.assertEqual(str(exc), 'foo') + def test_backend_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.BackendError, exceptions.MopidyException)) + def test_extension_error_is_a_mopidy_exception(self): self.assert_(issubclass( exceptions.ExtensionError, exceptions.MopidyException)) + def test_frontend_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.FrontendError, exceptions.MopidyException)) + + def test_mixer_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.MixerError, exceptions.MopidyException)) + def test_scanner_error_is_a_mopidy_exception(self): self.assert_(issubclass( exceptions.ScannerError, exceptions.MopidyException)) From dbecbbcea0bf3e4ec45bd52aa29a9056b09f9f19 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 23:20:10 +0200 Subject: [PATCH 08/10] main: Log and exit if {Backend,Frontend,Mixer}Error is raised (cherry picked from commit 95bddf666bedf1c8fd644042ae6fbf8adca43027) Conflicts: mopidy/commands.py --- mopidy/commands.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 022419a8..813d3f99 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -10,7 +10,7 @@ import glib import gobject -from mopidy import config as config_lib +from mopidy import config as config_lib, exceptions from mopidy.audio import Audio from mopidy.core import Core from mopidy.utils import deps, process, versioning @@ -270,9 +270,12 @@ class RootCommand(Command): core = self.start_core(audio, backends) self.start_frontends(config, frontend_classes, core) loop.run() + except (exceptions.BackendError, + exceptions.FrontendError, + exceptions.MixerError): + logger.info('Initialization error. Exiting...') except KeyboardInterrupt: logger.info('Interrupted. Exiting...') - return finally: loop.quit() self.stop_frontends(frontend_classes) @@ -292,8 +295,15 @@ class RootCommand(Command): backends = [] for backend_class in backend_classes: - backend = backend_class.start(config=config, audio=audio).proxy() - backends.append(backend) + try: + backend = backend_class.start( + config=config, audio=audio).proxy() + backends.append(backend) + except exceptions.BackendError as exc: + logger.error( + 'Backend (%s) initialization error: %s', + backend_class.__name__, exc.message) + raise return backends @@ -307,7 +317,13 @@ class RootCommand(Command): ', '.join(f.__name__ for f in frontend_classes) or 'none') for frontend_class in frontend_classes: - frontend_class.start(config=config, core=core) + try: + frontend_class.start(config=config, core=core) + except exceptions.FrontendError as exc: + logger.error( + 'Frontend (%s) initialization error: %s', + frontend_class.__name__, exc.message) + raise def stop_frontends(self, frontend_classes): logger.info('Stopping Mopidy frontends') From a1848aece50be42df55019914d3b9db61e11573f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 23:20:48 +0200 Subject: [PATCH 09/10] docs: Add {Backend,Frontend,Mixer}Error guidelines to backend/frontend/mixer APIs (cherry picked from commit 83f1d00944eaa2d22bffd9fc35ee3a9d990dfe41) Conflicts: mopidy/mixer.py --- docs/api/frontends.rst | 8 ++++---- mopidy/backend/__init__.py | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst index 5e2f8d6c..b843692d 100644 --- a/docs/api/frontends.rst +++ b/docs/api/frontends.rst @@ -29,12 +29,12 @@ The following requirements applies to any frontend implementation: - The main actor MUST be able to start and stop the frontend when the main actor is started and stopped. -- The frontend MAY require additional settings to be set for it to - work. +- The frontend MAY require additional config values to be set for it to work. -- Such settings MUST be documented. +- Such config values MUST be documented. -- The main actor MUST stop itself if the defined settings are not adequate for +- The main actor MUST raise the :exc:`~mopidy.exceptions.FrontendError` with a + descriptive error message if the defined config values are not adequate for the frontend to work properly. - Any actor which is part of the frontend MAY implement the diff --git a/mopidy/backend/__init__.py b/mopidy/backend/__init__.py index 6f895985..68aad778 100644 --- a/mopidy/backend/__init__.py +++ b/mopidy/backend/__init__.py @@ -6,6 +6,14 @@ from mopidy import listener class Backend(object): + """Backend API + + If the backend has problems during initialization it should raise + :exc:`~mopidy.exceptions.BackendError` with a descriptive error message. + This will make Mopidy print the error message and exit so that the user can + fix the issue. + """ + #: Actor proxy to an instance of :class:`mopidy.audio.Audio`. #: #: Should be passed to the backend constructor as the kwarg ``audio``, From d1f2146b59af4e7944fe155ea1e4837f07c67709 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 16 Jul 2014 10:36:46 +0200 Subject: [PATCH 10/10] mpd: Raise FrontendError instead of sys.exit(1) --- mopidy/mpd/actor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mopidy/mpd/actor.py b/mopidy/mpd/actor.py index 684b4968..52cf6746 100644 --- a/mopidy/mpd/actor.py +++ b/mopidy/mpd/actor.py @@ -1,11 +1,10 @@ from __future__ import unicode_literals import logging -import sys import pykka -from mopidy import zeroconf +from mopidy import exceptions, zeroconf from mopidy.core import CoreListener from mopidy.mpd import session from mopidy.utils import encoding, network, process @@ -34,10 +33,9 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): max_connections=config['mpd']['max_connections'], timeout=config['mpd']['connection_timeout']) except IOError as error: - logger.error( - 'MPD server startup failed: %s', + raise exceptions.FrontendError( + 'MPD server startup failed: %s' % encoding.locale_decode(error)) - sys.exit(1) logger.info('MPD server running at [%s]:%s', self.hostname, self.port)