Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions homeassistant/components/system_log/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import voluptuous as vol

from homeassistant import __path__ as HOMEASSISTANT_PATH
from homeassistant.components.http import HomeAssistantView
from homeassistant.components import websocket_api
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, ServiceCall, callback
import homeassistant.helpers.config_validation as cv
Expand Down Expand Up @@ -224,7 +224,7 @@ def _async_stop_queue_handler(_) -> None:

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler)

hass.http.register_view(AllErrorsView(handler))
websocket_api.async_register_command(hass, list_errors)

async def async_service_handler(service: ServiceCall) -> None:
"""Handle logger services."""
Expand Down Expand Up @@ -255,16 +255,14 @@ async def async_shutdown_handler(event):
return True


class AllErrorsView(HomeAssistantView):
"""Get all logged errors and warnings."""

url = "/api/error/all"
name = "api:error:all"

def __init__(self, handler):
"""Initialize a new AllErrorsView."""
self.handler = handler

async def get(self, request):
"""Get all errors and warnings."""
return self.json(self.handler.records.to_list())
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "system_log/list"})
@callback
def list_errors(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
):
"""List all possible diagnostic handlers."""
connection.send_result(
msg["id"],
hass.data[DOMAIN].records.to_list(),
)
2 changes: 1 addition & 1 deletion homeassistant/components/system_log/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"domain": "system_log",
"name": "System Log",
"documentation": "https://www.home-assistant.io/integrations/system_log",
"dependencies": ["http"],
"dependencies": [],
"codeowners": [],
"quality_scale": "internal"
}
91 changes: 49 additions & 42 deletions tests/components/system_log/test_init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Test system log component."""
import asyncio
from http import HTTPStatus
import logging
import queue
from unittest.mock import MagicMock, patch
Expand All @@ -11,6 +10,8 @@
from homeassistant.components import system_log
from homeassistant.core import callback

from tests.common import async_capture_events

_LOGGER = logging.getLogger("test_logger")
BASIC_CONFIG = {"system_log": {"max_entries": 2}}

Expand All @@ -34,19 +35,19 @@ async def _async_block_until_queue_empty(hass, sq):
hass.data[system_log.DOMAIN].acquire()
hass.data[system_log.DOMAIN].release()
await hass.async_block_till_done()
await hass.async_block_till_done()


async def get_error_log(hass, hass_client, expected_count):
async def get_error_log(hass_ws_client):
"""Fetch all entries from system_log via the API."""
client = await hass_ws_client()
await client.send_json({"id": 5, "type": "system_log/list"})

client = await hass_client()
resp = await client.get("/api/error/all")
assert resp.status == HTTPStatus.OK
msg = await client.receive_json()

data = await resp.json()

assert len(data) == expected_count
return data
assert msg["id"] == 5
assert msg["success"]
return msg["result"]


def _generate_and_log_exception(exception, log):
Expand All @@ -56,6 +57,18 @@ def _generate_and_log_exception(exception, log):
_LOGGER.exception(log)


def find_log(logs, level):
"""Return log with specific level."""
if not isinstance(level, tuple):
level = (level,)
log = next(
(log for log in logs if log["level"] in level),
None,
)
assert log is not None
return log


def assert_log(log, exception, message, level):
"""Assert that specified values are in a specific log entry."""
if not isinstance(message, list):
Expand All @@ -73,7 +86,7 @@ def get_frame(name):
return (name, 5, None, None)


async def test_normal_logs(hass, simple_queue, hass_client):
async def test_normal_logs(hass, simple_queue, hass_ws_client):
"""Test that debug and info are not logged."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)

Expand All @@ -82,36 +95,37 @@ async def test_normal_logs(hass, simple_queue, hass_client):
await _async_block_until_queue_empty(hass, simple_queue)

# Assert done by get_error_log
await get_error_log(hass, hass_client, 0)
logs = await get_error_log(hass_ws_client)
assert len([msg for msg in logs if msg["level"] in ("DEBUG", "INFO")]) == 0


async def test_exception(hass, simple_queue, hass_client):
async def test_exception(hass, simple_queue, hass_ws_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")
await _async_block_until_queue_empty(hass, simple_queue)

log = (await get_error_log(hass, hass_client, 1))[0]
log = find_log(await get_error_log(hass_ws_client), "ERROR")
assert log is not None
assert_log(log, "exception message", "log message", "ERROR")


async def test_warning(hass, simple_queue, hass_client):
async def test_warning(hass, simple_queue, hass_ws_client):
"""Test that warning are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.warning("warning message")
await _async_block_until_queue_empty(hass, simple_queue)

log = (await get_error_log(hass, hass_client, 1))[0]
log = find_log(await get_error_log(hass_ws_client), "WARNING")
assert_log(log, "", "warning message", "WARNING")


async def test_error(hass, simple_queue, hass_client):
async def test_error(hass, simple_queue, hass_ws_client):
"""Test that errors are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.error("error message")
await _async_block_until_queue_empty(hass, simple_queue)

log = (await get_error_log(hass, hass_client, 1))[0]
log = find_log(await get_error_log(hass_ws_client), "ERROR")
assert_log(log, "", "error message", "ERROR")


Expand All @@ -138,14 +152,7 @@ async def test_error_posted_as_event(hass, simple_queue):
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)
events = async_capture_events(hass, system_log.EVENT_SYSTEM_LOG)

_LOGGER.error("error message")
await _async_block_until_queue_empty(hass, simple_queue)
Expand All @@ -154,25 +161,25 @@ def event_listener(event):
assert_log(events[0].data, "", "error message", "ERROR")


async def test_critical(hass, simple_queue, hass_client):
async def test_critical(hass, simple_queue, hass_ws_client):
"""Test that critical are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.critical("critical message")
await _async_block_until_queue_empty(hass, simple_queue)

log = (await get_error_log(hass, hass_client, 1))[0]
log = find_log(await get_error_log(hass_ws_client), "CRITICAL")
assert_log(log, "", "critical message", "CRITICAL")


async def test_remove_older_logs(hass, simple_queue, hass_client):
async def test_remove_older_logs(hass, simple_queue, hass_ws_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")
await _async_block_until_queue_empty(hass, simple_queue)

log = await get_error_log(hass, hass_client, 2)
log = await get_error_log(hass_ws_client)
assert_log(log[0], "", "error message 3", "ERROR")
assert_log(log[1], "", "error message 2", "ERROR")

Expand All @@ -182,7 +189,7 @@ def log_msg(nr=2):
_LOGGER.error("error message %s", nr)


async def test_dedupe_logs(hass, simple_queue, hass_client):
async def test_dedupe_logs(hass, simple_queue, hass_ws_client):
"""Test that duplicate log entries are dedupe."""
await async_setup_component(hass, system_log.DOMAIN, {})
_LOGGER.error("error message 1")
Expand All @@ -191,15 +198,15 @@ async def test_dedupe_logs(hass, simple_queue, hass_client):
_LOGGER.error("error message 3")
await _async_block_until_queue_empty(hass, simple_queue)

log = await get_error_log(hass, hass_client, 3)
log = await get_error_log(hass_ws_client)
assert_log(log[0], "", "error message 3", "ERROR")
assert log[1]["count"] == 2
assert_log(log[1], "", ["error message 2", "error message 2-2"], "ERROR")

log_msg()
await _async_block_until_queue_empty(hass, simple_queue)

log = await get_error_log(hass, hass_client, 3)
log = await get_error_log(hass_ws_client)
assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR")
assert log[0]["timestamp"] > log[0]["first_occurred"]

Expand All @@ -209,7 +216,7 @@ async def test_dedupe_logs(hass, simple_queue, hass_client):
log_msg("2-6")
await _async_block_until_queue_empty(hass, simple_queue)

log = await get_error_log(hass, hass_client, 3)
log = await get_error_log(hass_ws_client)
assert_log(
log[0],
"",
Expand All @@ -224,7 +231,7 @@ async def test_dedupe_logs(hass, simple_queue, hass_client):
)


async def test_clear_logs(hass, simple_queue, hass_client):
async def test_clear_logs(hass, simple_queue, hass_ws_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")
Expand All @@ -234,7 +241,7 @@ async def test_clear_logs(hass, simple_queue, hass_client):
await _async_block_until_queue_empty(hass, simple_queue)

# Assert done by get_error_log
await get_error_log(hass, hass_client, 0)
await get_error_log(hass_ws_client)


async def test_write_log(hass):
Expand Down Expand Up @@ -277,13 +284,13 @@ async def test_write_choose_level(hass):
assert logger.method_calls[0] == ("debug", ("test_message",))


async def test_unknown_path(hass, simple_queue, hass_client):
async def test_unknown_path(hass, simple_queue, hass_ws_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")
await _async_block_until_queue_empty(hass, simple_queue)
log = (await get_error_log(hass, hass_client, 1))[0]
log = (await get_error_log(hass_ws_client))[0]
assert log["source"] == ["unknown_path", 0]


Expand All @@ -307,7 +314,7 @@ async def async_log_error_from_test_path(hass, path, sq):
await _async_block_until_queue_empty(hass, sq)


async def test_homeassistant_path(hass, simple_queue, hass_client):
async def test_homeassistant_path(hass, simple_queue, hass_ws_client):
"""Test error logged from Home Assistant path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch(
Expand All @@ -317,16 +324,16 @@ async def test_homeassistant_path(hass, simple_queue, hass_client):
await async_log_error_from_test_path(
hass, "venv_path/homeassistant/component/component.py", simple_queue
)
log = (await get_error_log(hass, hass_client, 1))[0]
log = (await get_error_log(hass_ws_client))[0]
assert log["source"] == ["component/component.py", 5]


async def test_config_path(hass, simple_queue, hass_client):
async def test_config_path(hass, simple_queue, hass_ws_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"):
await async_log_error_from_test_path(
hass, "config/custom_component/test.py", simple_queue
)
log = (await get_error_log(hass, hass_client, 1))[0]
log = (await get_error_log(hass_ws_client))[0]
assert log["source"] == ["custom_component/test.py", 5]