Add service to change log levels (#6221)

* Add service to change log levels

* Fix review comments
This commit is contained in:
Pierre Ståhl 2017-02-26 23:15:44 +01:00 committed by Paulus Schoutsen
parent 86d4d10176
commit 9490cb4c8f
3 changed files with 104 additions and 23 deletions

View File

@ -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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/logger/ https://home-assistant.io/components/logger/
""" """
import asyncio
import logging import logging
import os
from collections import OrderedDict from collections import OrderedDict
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'logger' DOMAIN = 'logger'
DATA_LOGGER = 'logger'
SERVICE_SET_LEVEL = 'set_level'
LOGSEVERITY = { LOGSEVERITY = {
'CRITICAL': 50, 'CRITICAL': 50,
'FATAL': 50, 'FATAL': 50,
@ -29,6 +36,8 @@ LOGGER_LOGS = 'logs'
_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) _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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL,
@ -37,6 +46,11 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, 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): class HomeAssistantLogFilter(logging.Filter):
"""A log filter.""" """A log filter."""
@ -61,7 +75,8 @@ class HomeAssistantLogFilter(logging.Filter):
return record.levelno >= default return record.levelno >= default
def setup(hass, config=None): @asyncio.coroutine
def async_setup(hass, config):
"""Setup the logger component.""" """Setup the logger component."""
logfilter = {} logfilter = {}
@ -72,21 +87,26 @@ def setup(hass, config=None):
config.get(DOMAIN)[LOGGER_DEFAULT] config.get(DOMAIN)[LOGGER_DEFAULT]
] ]
# Compute log severity for components def set_log_levels(logpoints):
if LOGGER_LOGS in config.get(DOMAIN): """Set the specified log levels."""
for key, value in config.get(DOMAIN)[LOGGER_LOGS].items(): logs = {}
config.get(DOMAIN)[LOGGER_LOGS][key] = LOGSEVERITY[value]
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( sorted(
config.get(DOMAIN)[LOGGER_LOGS].items(), logs.items(),
key=lambda t: len(t[0]), key=lambda t: len(t[0]),
reverse=True reverse=True
) )
) )
logfilter[LOGGER_LOGS] = logs
logger = logging.getLogger('') logger = logging.getLogger('')
logger.setLevel(logging.NOTSET) logger.setLevel(logging.NOTSET)
@ -95,4 +115,21 @@ def setup(hass, config=None):
handler.setLevel(logging.NOTSET) handler.setLevel(logging.NOTSET)
handler.addFilter(HomeAssistantLogFilter(logfilter)) 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 return True

View File

@ -312,3 +312,7 @@ ffmpeg:
entity_id: entity_id:
description: Name(s) of entites that will restart. Platform dependent. description: Name(s) of entites that will restart. Platform dependent.
example: 'binary_sensor.ffmpeg_noise' example: 'binary_sensor.ffmpeg_noise'
logger:
set_level:
description: Set log level for components.

View File

@ -10,6 +10,14 @@ from tests.common import get_test_home_assistant
RECORD = namedtuple('record', ('name', 'levelno')) RECORD = namedtuple('record', ('name', 'levelno'))
NO_LOGS_CONFIG = {'logger': {'default': 'info'}}
TEST_CONFIG = {
'logger': {
'default': 'warning',
'logs': {'test': 'info'}
}
}
class TestUpdater(unittest.TestCase): class TestUpdater(unittest.TestCase):
"""Test logger component.""" """Test logger component."""
@ -17,17 +25,29 @@ class TestUpdater(unittest.TestCase):
def setUp(self): def setUp(self):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.log_config = {'logger': self.log_filter = None
{'default': 'warning', 'logs': {'test': 'info'}}}
def tearDown(self): def tearDown(self):
"""Stop everything that was started.""" """Stop everything that was started."""
del logging.root.handlers[-1] del logging.root.handlers[-1]
self.hass.stop() 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): def test_logger_setup(self):
"""Use logger to create a logging filter.""" """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) self.assertTrue(len(logging.root.handlers) > 0)
handler = logging.root.handlers[-1] handler = logging.root.handlers[-1]
@ -40,22 +60,42 @@ class TestUpdater(unittest.TestCase):
def test_logger_test_filters(self): def test_logger_test_filters(self):
"""Test resulting filter operation.""" """Test resulting filter operation."""
setup_component(self.hass, logger.DOMAIN, self.log_config) self.setup_logger(TEST_CONFIG)
log_filter = logging.root.handlers[-1].filters[0]
# Blocked default record # Blocked default record
record = RECORD('asdf', logging.DEBUG) self.assert_not_logged('asdf', logging.DEBUG)
self.assertFalse(log_filter.filter(record))
# Allowed default record # Allowed default record
record = RECORD('asdf', logging.WARNING) self.assert_logged('asdf', logging.WARNING)
self.assertTrue(log_filter.filter(record))
# Blocked named record # Blocked named record
record = RECORD('test', logging.DEBUG) self.assert_not_logged('test', logging.DEBUG)
self.assertFalse(log_filter.filter(record))
# Allowed named record # Allowed named record
record = RECORD('test', logging.INFO) self.assert_logged('test', logging.INFO)
self.assertTrue(log_filter.filter(record))
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)