System log: make firing event optional (#14102)
* Syste log: make firing event optional * Add test * Lint * Doc stringpull/14157/head
parent
b352b761f3
commit
93fe61bf13
|
@ -19,12 +19,14 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
CONF_MAX_ENTRIES = 'max_entries'
|
||||
CONF_FIRE_EVENT = 'fire_event'
|
||||
CONF_MESSAGE = 'message'
|
||||
CONF_LEVEL = 'level'
|
||||
CONF_LOGGER = 'logger'
|
||||
|
||||
DATA_SYSTEM_LOG = 'system_log'
|
||||
DEFAULT_MAX_ENTRIES = 50
|
||||
DEFAULT_FIRE_EVENT = False
|
||||
DEPENDENCIES = ['http']
|
||||
DOMAIN = 'system_log'
|
||||
|
||||
|
@ -37,6 +39,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=DEFAULT_FIRE_EVENT): cv.boolean,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -97,11 +100,12 @@ def _exception_as_string(exc_info):
|
|||
class LogErrorHandler(logging.Handler):
|
||||
"""Log handler for error messages."""
|
||||
|
||||
def __init__(self, hass, maxlen):
|
||||
def __init__(self, hass, maxlen, fire_event):
|
||||
"""Initialize a new LogErrorHandler."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self.records = deque(maxlen=maxlen)
|
||||
self.fire_event = fire_event
|
||||
|
||||
def _create_entry(self, record, call_stack):
|
||||
return {
|
||||
|
@ -130,7 +134,8 @@ class LogErrorHandler(logging.Handler):
|
|||
|
||||
entry = self._create_entry(record, stack)
|
||||
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
|
||||
|
@ -140,7 +145,8 @@ def async_setup(hass, config):
|
|||
if conf is None:
|
||||
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)
|
||||
|
||||
hass.http.register_view(AllErrorsView(handler))
|
||||
|
|
|
@ -1,33 +1,26 @@
|
|||
"""Test system log component."""
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components import system_log
|
||||
|
||||
_LOGGER = logging.getLogger('test_logger')
|
||||
BASIC_CONFIG = {
|
||||
'system_log': {
|
||||
'max_entries': 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@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):
|
||||
async def get_error_log(hass, aiohttp_client, expected_count):
|
||||
"""Fetch all entries from system_log via the API."""
|
||||
client = yield from aiohttp_client(hass.http.app)
|
||||
resp = yield from client.get('/api/error/all')
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
resp = await client.get('/api/error/all')
|
||||
assert resp.status == 200
|
||||
|
||||
data = yield from resp.json()
|
||||
data = await resp.json()
|
||||
assert len(data) == expected_count
|
||||
return data
|
||||
|
||||
|
@ -52,43 +45,43 @@ def get_frame(name):
|
|||
return (name, None, None, None)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_normal_logs(hass, aiohttp_client):
|
||||
async def test_normal_logs(hass, aiohttp_client):
|
||||
"""Test that debug and info are not logged."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.debug('debug')
|
||||
_LOGGER.info('info')
|
||||
|
||||
# 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
|
||||
def test_exception(hass, aiohttp_client):
|
||||
async def test_exception(hass, aiohttp_client):
|
||||
"""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')
|
||||
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')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_warning(hass, aiohttp_client):
|
||||
async def test_warning(hass, aiohttp_client):
|
||||
"""Test that warning are logged and retrieved correctly."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_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')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_error(hass, aiohttp_client):
|
||||
async def test_error(hass, aiohttp_client):
|
||||
"""Test that errors are logged and retrieved correctly."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_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')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_error_posted_as_event(hass, aiohttp_client):
|
||||
"""Test that error are posted as events."""
|
||||
async def test_config_not_fire_event(hass):
|
||||
"""Test that errors are not posted as events with default config."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
events = []
|
||||
|
||||
@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)
|
||||
|
||||
_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_log(events[0].data, '', 'error message', 'ERROR')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_critical(hass, aiohttp_client):
|
||||
async def test_critical(hass, aiohttp_client):
|
||||
"""Test that critical are logged and retrieved correctly."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_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')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_remove_older_logs(hass, aiohttp_client):
|
||||
async def test_remove_older_logs(hass, aiohttp_client):
|
||||
"""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 2')
|
||||
_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[1], '', 'error message 2', 'ERROR')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_clear_logs(hass, aiohttp_client):
|
||||
async def test_clear_logs(hass, aiohttp_client):
|
||||
"""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')
|
||||
|
||||
hass.async_add_job(
|
||||
hass.services.async_call(
|
||||
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
|
||||
yield from get_error_log(hass, aiohttp_client, 0)
|
||||
await get_error_log(hass, aiohttp_client, 0)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_write_log(hass):
|
||||
async def test_write_log(hass):
|
||||
"""Test that error propagates to logger."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
logger = MagicMock()
|
||||
with patch('logging.getLogger', return_value=logger) as mock_logging:
|
||||
hass.async_add_job(
|
||||
hass.services.async_call(
|
||||
system_log.DOMAIN, system_log.SERVICE_WRITE,
|
||||
{'message': 'test_message'}))
|
||||
yield from hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
mock_logging.assert_called_once_with(
|
||||
'homeassistant.components.system_log.external')
|
||||
assert logger.method_calls[0] == ('error', ('test_message',))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_write_choose_logger(hass):
|
||||
async def test_write_choose_logger(hass):
|
||||
"""Test that correct logger is chosen."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
with patch('logging.getLogger') as mock_logging:
|
||||
hass.async_add_job(
|
||||
hass.services.async_call(
|
||||
system_log.DOMAIN, system_log.SERVICE_WRITE,
|
||||
{'message': 'test_message',
|
||||
'logger': 'myLogger'}))
|
||||
yield from hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
mock_logging.assert_called_once_with(
|
||||
'myLogger')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_write_choose_level(hass):
|
||||
async def test_write_choose_level(hass):
|
||||
"""Test that correct logger is chosen."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
logger = MagicMock()
|
||||
with patch('logging.getLogger', return_value=logger):
|
||||
hass.async_add_job(
|
||||
|
@ -177,17 +193,17 @@ def test_write_choose_level(hass):
|
|||
system_log.DOMAIN, system_log.SERVICE_WRITE,
|
||||
{'message': 'test_message',
|
||||
'level': 'debug'}))
|
||||
yield from hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
assert logger.method_calls[0] == ('debug', ('test_message',))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_unknown_path(hass, aiohttp_client):
|
||||
async def test_unknown_path(hass, aiohttp_client):
|
||||
"""Test error logged from unknown path."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.findCaller = MagicMock(
|
||||
return_value=('unknown_path', 0, None, None))
|
||||
_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'
|
||||
|
||||
|
||||
|
@ -206,31 +222,31 @@ def log_error_from_test_path(path):
|
|||
_LOGGER.error('error message')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_homeassistant_path(hass, aiohttp_client):
|
||||
async def test_homeassistant_path(hass, aiohttp_client):
|
||||
"""Test error logged from homeassistant path."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH',
|
||||
new=['venv_path/homeassistant']):
|
||||
log_error_from_test_path(
|
||||
'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'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_config_path(hass, aiohttp_client):
|
||||
async def test_config_path(hass, aiohttp_client):
|
||||
"""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'):
|
||||
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'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_netdisco_path(hass, aiohttp_client):
|
||||
async def test_netdisco_path(hass, aiohttp_client):
|
||||
"""Test error logged from netdisco path."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
with patch.dict('sys.modules',
|
||||
netdisco=MagicMock(__path__=['venv_path/netdisco'])):
|
||||
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'
|
||||
|
|
Loading…
Reference in New Issue