diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 4bf163ff9eb..8572bbc044a 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -4,15 +4,22 @@ Component that will help set the level of logging for components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logger/ """ +import asyncio import logging +import os from collections import OrderedDict import voluptuous as vol +from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv DOMAIN = 'logger' +DATA_LOGGER = 'logger' + +SERVICE_SET_LEVEL = 'set_level' + LOGSEVERITY = { 'CRITICAL': 50, 'FATAL': 50, @@ -29,6 +36,8 @@ LOGGER_LOGS = 'logs' _VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) +SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, @@ -37,6 +46,11 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +def set_level(hass, logs): + """Set log level for components.""" + hass.services.call(DOMAIN, SERVICE_SET_LEVEL, logs) + + class HomeAssistantLogFilter(logging.Filter): """A log filter.""" @@ -61,7 +75,8 @@ class HomeAssistantLogFilter(logging.Filter): return record.levelno >= default -def setup(hass, config=None): +@asyncio.coroutine +def async_setup(hass, config): """Setup the logger component.""" logfilter = {} @@ -72,21 +87,26 @@ def setup(hass, config=None): config.get(DOMAIN)[LOGGER_DEFAULT] ] - # Compute log severity for components - if LOGGER_LOGS in config.get(DOMAIN): - for key, value in config.get(DOMAIN)[LOGGER_LOGS].items(): - config.get(DOMAIN)[LOGGER_LOGS][key] = LOGSEVERITY[value] + def set_log_levels(logpoints): + """Set the specified log levels.""" + logs = {} - logs = OrderedDict( + # Preserve existing logs + if LOGGER_LOGS in logfilter: + logs.update(logfilter[LOGGER_LOGS]) + + # Add new logpoints mapped to correc severity + for key, value in logpoints.items(): + logs[key] = LOGSEVERITY[value] + + logfilter[LOGGER_LOGS] = OrderedDict( sorted( - config.get(DOMAIN)[LOGGER_LOGS].items(), + logs.items(), key=lambda t: len(t[0]), reverse=True ) ) - logfilter[LOGGER_LOGS] = logs - logger = logging.getLogger('') logger.setLevel(logging.NOTSET) @@ -95,4 +115,21 @@ def setup(hass, config=None): handler.setLevel(logging.NOTSET) handler.addFilter(HomeAssistantLogFilter(logfilter)) + if LOGGER_LOGS in config.get(DOMAIN): + set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) + + @asyncio.coroutine + def async_service_handler(service): + """Handle logger services.""" + set_log_levels(service.data) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_SET_LEVEL, async_service_handler, + descriptions[DOMAIN].get(SERVICE_SET_LEVEL), + schema=SERVICE_SET_LEVEL_SCHEMA) + return True diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 661f8be8dab..a28a95969fb 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -312,3 +312,7 @@ ffmpeg: entity_id: description: Name(s) of entites that will restart. Platform dependent. example: 'binary_sensor.ffmpeg_noise' + +logger: + set_level: + description: Set log level for components. diff --git a/tests/components/test_logger.py b/tests/components/test_logger.py index e4e8c75d1bd..099137bdf4b 100644 --- a/tests/components/test_logger.py +++ b/tests/components/test_logger.py @@ -10,6 +10,14 @@ from tests.common import get_test_home_assistant RECORD = namedtuple('record', ('name', 'levelno')) +NO_LOGS_CONFIG = {'logger': {'default': 'info'}} +TEST_CONFIG = { + 'logger': { + 'default': 'warning', + 'logs': {'test': 'info'} + } +} + class TestUpdater(unittest.TestCase): """Test logger component.""" @@ -17,17 +25,29 @@ class TestUpdater(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.log_config = {'logger': - {'default': 'warning', 'logs': {'test': 'info'}}} + self.log_filter = None def tearDown(self): """Stop everything that was started.""" del logging.root.handlers[-1] self.hass.stop() + def setup_logger(self, config): + """Setup logger and save log filter.""" + setup_component(self.hass, logger.DOMAIN, config) + self.log_filter = logging.root.handlers[-1].filters[0] + + def assert_logged(self, name, level): + """Assert that a certain record was logged.""" + self.assertTrue(self.log_filter.filter(RECORD(name, level))) + + def assert_not_logged(self, name, level): + """Assert that a certain record was not logged.""" + self.assertFalse(self.log_filter.filter(RECORD(name, level))) + def test_logger_setup(self): """Use logger to create a logging filter.""" - setup_component(self.hass, logger.DOMAIN, self.log_config) + self.setup_logger(TEST_CONFIG) self.assertTrue(len(logging.root.handlers) > 0) handler = logging.root.handlers[-1] @@ -40,22 +60,42 @@ class TestUpdater(unittest.TestCase): def test_logger_test_filters(self): """Test resulting filter operation.""" - setup_component(self.hass, logger.DOMAIN, self.log_config) - - log_filter = logging.root.handlers[-1].filters[0] + self.setup_logger(TEST_CONFIG) # Blocked default record - record = RECORD('asdf', logging.DEBUG) - self.assertFalse(log_filter.filter(record)) + self.assert_not_logged('asdf', logging.DEBUG) # Allowed default record - record = RECORD('asdf', logging.WARNING) - self.assertTrue(log_filter.filter(record)) + self.assert_logged('asdf', logging.WARNING) # Blocked named record - record = RECORD('test', logging.DEBUG) - self.assertFalse(log_filter.filter(record)) + self.assert_not_logged('test', logging.DEBUG) # Allowed named record - record = RECORD('test', logging.INFO) - self.assertTrue(log_filter.filter(record)) + self.assert_logged('test', logging.INFO) + + def test_set_filter_empty_config(self): + """Test change log level from empty configuration.""" + self.setup_logger(NO_LOGS_CONFIG) + + self.assert_not_logged('test', logging.DEBUG) + + self.hass.services.call( + logger.DOMAIN, 'set_level', {'test': 'debug'}) + self.hass.block_till_done() + + self.assert_logged('test', logging.DEBUG) + + def test_set_filter(self): + """Test change log level of existing filter.""" + self.setup_logger(TEST_CONFIG) + + self.assert_not_logged('asdf', logging.DEBUG) + self.assert_logged('dummy', logging.WARNING) + + self.hass.services.call(logger.DOMAIN, 'set_level', + {'asdf': 'debug', 'dummy': 'info'}) + self.hass.block_till_done() + + self.assert_logged('asdf', logging.DEBUG) + self.assert_logged('dummy', logging.WARNING)