System log: make firing event optional (#14102)

* Syste log: make firing event optional

* Add test

* Lint

* Doc string
This commit is contained in:
Paulus Schoutsen 2018-04-28 17:09:38 -04:00 committed by GitHub
parent b352b761f3
commit 93fe61bf13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 88 additions and 66 deletions

View File

@ -19,12 +19,14 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
CONF_MAX_ENTRIES = 'max_entries' CONF_MAX_ENTRIES = 'max_entries'
CONF_FIRE_EVENT = 'fire_event'
CONF_MESSAGE = 'message' CONF_MESSAGE = 'message'
CONF_LEVEL = 'level' CONF_LEVEL = 'level'
CONF_LOGGER = 'logger' CONF_LOGGER = 'logger'
DATA_SYSTEM_LOG = 'system_log' DATA_SYSTEM_LOG = 'system_log'
DEFAULT_MAX_ENTRIES = 50 DEFAULT_MAX_ENTRIES = 50
DEFAULT_FIRE_EVENT = False
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
DOMAIN = 'system_log' DOMAIN = 'system_log'
@ -37,6 +39,7 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES):
cv.positive_int, cv.positive_int,
vol.Optional(CONF_FIRE_EVENT, default=DEFAULT_FIRE_EVENT): cv.boolean,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -97,11 +100,12 @@ def _exception_as_string(exc_info):
class LogErrorHandler(logging.Handler): class LogErrorHandler(logging.Handler):
"""Log handler for error messages.""" """Log handler for error messages."""
def __init__(self, hass, maxlen): def __init__(self, hass, maxlen, fire_event):
"""Initialize a new LogErrorHandler.""" """Initialize a new LogErrorHandler."""
super().__init__() super().__init__()
self.hass = hass self.hass = hass
self.records = deque(maxlen=maxlen) self.records = deque(maxlen=maxlen)
self.fire_event = fire_event
def _create_entry(self, record, call_stack): def _create_entry(self, record, call_stack):
return { return {
@ -130,7 +134,8 @@ class LogErrorHandler(logging.Handler):
entry = self._create_entry(record, stack) entry = self._create_entry(record, stack)
self.records.appendleft(entry) self.records.appendleft(entry)
self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) if self.fire_event:
self.hass.bus.fire(EVENT_SYSTEM_LOG, entry)
@asyncio.coroutine @asyncio.coroutine
@ -140,7 +145,8 @@ def async_setup(hass, config):
if conf is None: if conf is None:
conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES)) handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES],
conf[CONF_FIRE_EVENT])
logging.getLogger().addHandler(handler) logging.getLogger().addHandler(handler)
hass.http.register_view(AllErrorsView(handler)) hass.http.register_view(AllErrorsView(handler))

View File

@ -1,33 +1,26 @@
"""Test system log component.""" """Test system log component."""
import asyncio
import logging import logging
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.bootstrap import async_setup_component from homeassistant.bootstrap import async_setup_component
from homeassistant.components import system_log from homeassistant.components import system_log
_LOGGER = logging.getLogger('test_logger') _LOGGER = logging.getLogger('test_logger')
BASIC_CONFIG = {
'system_log': {
'max_entries': 2,
}
}
@pytest.fixture(autouse=True) async def get_error_log(hass, aiohttp_client, expected_count):
@asyncio.coroutine
def setup_test_case(hass, aiohttp_client):
"""Setup system_log component before test case."""
config = {'system_log': {'max_entries': 2}}
yield from async_setup_component(hass, system_log.DOMAIN, config)
@asyncio.coroutine
def get_error_log(hass, aiohttp_client, expected_count):
"""Fetch all entries from system_log via the API.""" """Fetch all entries from system_log via the API."""
client = yield from aiohttp_client(hass.http.app) client = await aiohttp_client(hass.http.app)
resp = yield from client.get('/api/error/all') resp = await client.get('/api/error/all')
assert resp.status == 200 assert resp.status == 200
data = yield from resp.json() data = await resp.json()
assert len(data) == expected_count assert len(data) == expected_count
return data return data
@ -52,43 +45,43 @@ def get_frame(name):
return (name, None, None, None) return (name, None, None, None)
@asyncio.coroutine async def test_normal_logs(hass, aiohttp_client):
def test_normal_logs(hass, aiohttp_client):
"""Test that debug and info are not logged.""" """Test that debug and info are not logged."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.debug('debug') _LOGGER.debug('debug')
_LOGGER.info('info') _LOGGER.info('info')
# Assert done by get_error_log # Assert done by get_error_log
yield from get_error_log(hass, aiohttp_client, 0) await get_error_log(hass, aiohttp_client, 0)
@asyncio.coroutine async def test_exception(hass, aiohttp_client):
def test_exception(hass, aiohttp_client):
"""Test that exceptions are logged and retrieved correctly.""" """Test that exceptions are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_generate_and_log_exception('exception message', 'log message') _generate_and_log_exception('exception message', 'log message')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert_log(log, 'exception message', 'log message', 'ERROR') assert_log(log, 'exception message', 'log message', 'ERROR')
@asyncio.coroutine async def test_warning(hass, aiohttp_client):
def test_warning(hass, aiohttp_client):
"""Test that warning are logged and retrieved correctly.""" """Test that warning are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.warning('warning message') _LOGGER.warning('warning message')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert_log(log, '', 'warning message', 'WARNING') assert_log(log, '', 'warning message', 'WARNING')
@asyncio.coroutine async def test_error(hass, aiohttp_client):
def test_error(hass, aiohttp_client):
"""Test that errors are logged and retrieved correctly.""" """Test that errors are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.error('error message') _LOGGER.error('error message')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert_log(log, '', 'error message', 'ERROR') assert_log(log, '', 'error message', 'ERROR')
@asyncio.coroutine async def test_config_not_fire_event(hass):
def test_error_posted_as_event(hass, aiohttp_client): """Test that errors are not posted as events with default config."""
"""Test that error are posted as events.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
events = [] events = []
@callback @callback
@ -99,77 +92,100 @@ def test_error_posted_as_event(hass, aiohttp_client):
hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener)
_LOGGER.error('error message') _LOGGER.error('error message')
yield from hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 0
async def test_error_posted_as_event(hass):
"""Test that error are posted as events."""
await async_setup_component(hass, system_log.DOMAIN, {
'system_log': {
'max_entries': 2,
'fire_event': True,
}
})
events = []
@callback
def event_listener(event):
"""Listen to events of type system_log_event."""
events.append(event)
hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener)
_LOGGER.error('error message')
await hass.async_block_till_done()
assert len(events) == 1 assert len(events) == 1
assert_log(events[0].data, '', 'error message', 'ERROR') assert_log(events[0].data, '', 'error message', 'ERROR')
@asyncio.coroutine async def test_critical(hass, aiohttp_client):
def test_critical(hass, aiohttp_client):
"""Test that critical are logged and retrieved correctly.""" """Test that critical are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.critical('critical message') _LOGGER.critical('critical message')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert_log(log, '', 'critical message', 'CRITICAL') assert_log(log, '', 'critical message', 'CRITICAL')
@asyncio.coroutine async def test_remove_older_logs(hass, aiohttp_client):
def test_remove_older_logs(hass, aiohttp_client):
"""Test that older logs are rotated out.""" """Test that older logs are rotated out."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.error('error message 1') _LOGGER.error('error message 1')
_LOGGER.error('error message 2') _LOGGER.error('error message 2')
_LOGGER.error('error message 3') _LOGGER.error('error message 3')
log = yield from get_error_log(hass, aiohttp_client, 2) log = await get_error_log(hass, aiohttp_client, 2)
assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[0], '', 'error message 3', 'ERROR')
assert_log(log[1], '', 'error message 2', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR')
@asyncio.coroutine async def test_clear_logs(hass, aiohttp_client):
def test_clear_logs(hass, aiohttp_client):
"""Test that the log can be cleared via a service call.""" """Test that the log can be cleared via a service call."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.error('error message') _LOGGER.error('error message')
hass.async_add_job( hass.async_add_job(
hass.services.async_call( hass.services.async_call(
system_log.DOMAIN, system_log.SERVICE_CLEAR, {})) system_log.DOMAIN, system_log.SERVICE_CLEAR, {}))
yield from hass.async_block_till_done() await hass.async_block_till_done()
# Assert done by get_error_log # Assert done by get_error_log
yield from get_error_log(hass, aiohttp_client, 0) await get_error_log(hass, aiohttp_client, 0)
@asyncio.coroutine async def test_write_log(hass):
def test_write_log(hass):
"""Test that error propagates to logger.""" """Test that error propagates to logger."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
logger = MagicMock() logger = MagicMock()
with patch('logging.getLogger', return_value=logger) as mock_logging: with patch('logging.getLogger', return_value=logger) as mock_logging:
hass.async_add_job( hass.async_add_job(
hass.services.async_call( hass.services.async_call(
system_log.DOMAIN, system_log.SERVICE_WRITE, system_log.DOMAIN, system_log.SERVICE_WRITE,
{'message': 'test_message'})) {'message': 'test_message'}))
yield from hass.async_block_till_done() await hass.async_block_till_done()
mock_logging.assert_called_once_with( mock_logging.assert_called_once_with(
'homeassistant.components.system_log.external') 'homeassistant.components.system_log.external')
assert logger.method_calls[0] == ('error', ('test_message',)) assert logger.method_calls[0] == ('error', ('test_message',))
@asyncio.coroutine async def test_write_choose_logger(hass):
def test_write_choose_logger(hass):
"""Test that correct logger is chosen.""" """Test that correct logger is chosen."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch('logging.getLogger') as mock_logging: with patch('logging.getLogger') as mock_logging:
hass.async_add_job( hass.async_add_job(
hass.services.async_call( hass.services.async_call(
system_log.DOMAIN, system_log.SERVICE_WRITE, system_log.DOMAIN, system_log.SERVICE_WRITE,
{'message': 'test_message', {'message': 'test_message',
'logger': 'myLogger'})) 'logger': 'myLogger'}))
yield from hass.async_block_till_done() await hass.async_block_till_done()
mock_logging.assert_called_once_with( mock_logging.assert_called_once_with(
'myLogger') 'myLogger')
@asyncio.coroutine async def test_write_choose_level(hass):
def test_write_choose_level(hass):
"""Test that correct logger is chosen.""" """Test that correct logger is chosen."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
logger = MagicMock() logger = MagicMock()
with patch('logging.getLogger', return_value=logger): with patch('logging.getLogger', return_value=logger):
hass.async_add_job( hass.async_add_job(
@ -177,17 +193,17 @@ def test_write_choose_level(hass):
system_log.DOMAIN, system_log.SERVICE_WRITE, system_log.DOMAIN, system_log.SERVICE_WRITE,
{'message': 'test_message', {'message': 'test_message',
'level': 'debug'})) 'level': 'debug'}))
yield from hass.async_block_till_done() await hass.async_block_till_done()
assert logger.method_calls[0] == ('debug', ('test_message',)) assert logger.method_calls[0] == ('debug', ('test_message',))
@asyncio.coroutine async def test_unknown_path(hass, aiohttp_client):
def test_unknown_path(hass, aiohttp_client):
"""Test error logged from unknown path.""" """Test error logged from unknown path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.findCaller = MagicMock( _LOGGER.findCaller = MagicMock(
return_value=('unknown_path', 0, None, None)) return_value=('unknown_path', 0, None, None))
_LOGGER.error('error message') _LOGGER.error('error message')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert log['source'] == 'unknown_path' assert log['source'] == 'unknown_path'
@ -206,31 +222,31 @@ def log_error_from_test_path(path):
_LOGGER.error('error message') _LOGGER.error('error message')
@asyncio.coroutine async def test_homeassistant_path(hass, aiohttp_client):
def test_homeassistant_path(hass, aiohttp_client):
"""Test error logged from homeassistant path.""" """Test error logged from homeassistant path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH',
new=['venv_path/homeassistant']): new=['venv_path/homeassistant']):
log_error_from_test_path( log_error_from_test_path(
'venv_path/homeassistant/component/component.py') 'venv_path/homeassistant/component/component.py')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert log['source'] == 'component/component.py' assert log['source'] == 'component/component.py'
@asyncio.coroutine async def test_config_path(hass, aiohttp_client):
def test_config_path(hass, aiohttp_client):
"""Test error logged from config path.""" """Test error logged from config path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch.object(hass.config, 'config_dir', new='config'): with patch.object(hass.config, 'config_dir', new='config'):
log_error_from_test_path('config/custom_component/test.py') log_error_from_test_path('config/custom_component/test.py')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert log['source'] == 'custom_component/test.py' assert log['source'] == 'custom_component/test.py'
@asyncio.coroutine async def test_netdisco_path(hass, aiohttp_client):
def test_netdisco_path(hass, aiohttp_client):
"""Test error logged from netdisco path.""" """Test error logged from netdisco path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch.dict('sys.modules', with patch.dict('sys.modules',
netdisco=MagicMock(__path__=['venv_path/netdisco'])): netdisco=MagicMock(__path__=['venv_path/netdisco'])):
log_error_from_test_path('venv_path/netdisco/disco_component.py') log_error_from_test_path('venv_path/netdisco/disco_component.py')
log = (yield from get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, aiohttp_client, 1))[0]
assert log['source'] == 'disco_component.py' assert log['source'] == 'disco_component.py'