diff --git a/.strict-typing b/.strict-typing index 4778893bfc2..08a167855d1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -174,6 +174,7 @@ homeassistant.components.litterrobot.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.logbook.* +homeassistant.components.logger.* homeassistant.components.lookin.* homeassistant.components.luftdaten.* homeassistant.components.mailbox.* diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index 5fc999d7d11..0d087ef23b7 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,5 +1,8 @@ """Support for setting the level of logging for components.""" +from __future__ import annotations + import logging +import re import voluptuous as vol @@ -7,29 +10,26 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -DOMAIN = "logger" +from . import websocket_api +from .const import ( + ATTR_LEVEL, + DEFAULT_LOGSEVERITY, + DOMAIN, + LOGGER_DEFAULT, + LOGGER_FILTERS, + LOGGER_LOGS, + LOGSEVERITY, + SERVICE_SET_DEFAULT_LEVEL, + SERVICE_SET_LEVEL, +) +from .helpers import ( + LoggerDomainConfig, + LoggerSettings, + set_default_log_level, + set_log_levels, +) -SERVICE_SET_DEFAULT_LEVEL = "set_default_level" -SERVICE_SET_LEVEL = "set_level" - -LOGSEVERITY = { - "CRITICAL": 50, - "FATAL": 50, - "ERROR": 40, - "WARNING": 30, - "WARN": 30, - "INFO": 20, - "DEBUG": 10, - "NOTSET": 0, -} - -LOGGER_DEFAULT = "default" -LOGGER_LOGS = "logs" -LOGGER_FILTERS = "filters" - -ATTR_LEVEL = "level" - -_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) +_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY), LOGSEVERITY.__getitem__) SERVICE_SET_DEFAULT_LEVEL_SCHEMA = vol.Schema({ATTR_LEVEL: _VALID_LOG_LEVEL}) SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL}) @@ -38,7 +38,9 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, + vol.Optional( + LOGGER_DEFAULT, default=DEFAULT_LOGSEVERITY + ): _VALID_LOG_LEVEL, vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}), vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}), } @@ -50,42 +52,38 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the logger component.""" - hass.data[DOMAIN] = {} - logging.setLoggerClass(_get_logger_class(hass.data[DOMAIN])) - @callback - def set_default_log_level(level): - """Set the default log level for components.""" - _set_log_level(logging.getLogger(""), level) + settings = LoggerSettings(hass, config) - @callback - def set_log_levels(logpoints): - """Set the specified log levels.""" - hass.data[DOMAIN].update(logpoints) - for key, value in logpoints.items(): - _set_log_level(logging.getLogger(key), value) + domain_config = hass.data[DOMAIN] = LoggerDomainConfig({}, settings) + logging.setLoggerClass(_get_logger_class(domain_config.overrides)) - # Set default log severity + websocket_api.async_load_websocket_api(hass) + + await settings.async_load() + + # Set default log severity and filter logger_config = config.get(DOMAIN, {}) if LOGGER_DEFAULT in logger_config: - set_default_log_level(logger_config[LOGGER_DEFAULT]) - - if LOGGER_LOGS in logger_config: - set_log_levels(config[DOMAIN][LOGGER_LOGS]) + set_default_log_level(hass, logger_config[LOGGER_DEFAULT]) if LOGGER_FILTERS in logger_config: - for key, value in logger_config[LOGGER_FILTERS].items(): - logger = logging.getLogger(key) - _add_log_filter(logger, value) + log_filters: dict[str, list[re.Pattern]] = logger_config[LOGGER_FILTERS] + for key, value in log_filters.items(): + _add_log_filter(logging.getLogger(key), value) + + # Combine log levels configured in configuration.yaml with log levels set by frontend + combined_logs = await settings.async_get_levels(hass) + set_log_levels(hass, combined_logs) @callback def async_service_handler(service: ServiceCall) -> None: """Handle logger services.""" if service.service == SERVICE_SET_DEFAULT_LEVEL: - set_default_log_level(service.data.get(ATTR_LEVEL)) + set_default_log_level(hass, service.data[ATTR_LEVEL]) else: - set_log_levels(service.data) + set_log_levels(hass, service.data) hass.services.async_register( DOMAIN, @@ -104,24 +102,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _set_log_level(logger, level): - """Set the log level. - - Any logger fetched before this integration is loaded will use old class. - """ - getattr(logger, "orig_setLevel", logger.setLevel)(LOGSEVERITY[level]) - - -def _add_log_filter(logger, patterns): +def _add_log_filter(logger: logging.Logger, patterns: list[re.Pattern]) -> None: """Add a Filter to the logger based on a regexp of the filter_str.""" - def filter_func(logrecord): + def filter_func(logrecord: logging.LogRecord) -> bool: return not any(p.search(logrecord.getMessage()) for p in patterns) logger.addFilter(filter_func) -def _get_logger_class(hass_overrides): +def _get_logger_class(hass_overrides: dict[str, int]) -> type[logging.Logger]: """Create a logger subclass. logging.setLoggerClass checks if it is a subclass of Logger and @@ -131,7 +121,7 @@ def _get_logger_class(hass_overrides): class HassLogger(logging.Logger): """Home Assistant aware logger class.""" - def setLevel(self, level) -> None: + def setLevel(self, level: int | str) -> None: """Set the log level unless overridden.""" if self.name in hass_overrides: return @@ -139,7 +129,7 @@ def _get_logger_class(hass_overrides): super().setLevel(level) # pylint: disable=invalid-name - def orig_setLevel(self, level) -> None: + def orig_setLevel(self, level: int | str) -> None: """Set the log level.""" super().setLevel(level) diff --git a/homeassistant/components/logger/const.py b/homeassistant/components/logger/const.py new file mode 100644 index 00000000000..06f2af4f3f5 --- /dev/null +++ b/homeassistant/components/logger/const.py @@ -0,0 +1,42 @@ +"""Constants for the Logger integration.""" +import logging + +DOMAIN = "logger" + +SERVICE_SET_DEFAULT_LEVEL = "set_default_level" +SERVICE_SET_LEVEL = "set_level" + +LOGSEVERITY_NOTSET = "NOTSET" +LOGSEVERITY_DEBUG = "DEBUG" +LOGSEVERITY_INFO = "INFO" +LOGSEVERITY_WARNING = "WARNING" +LOGSEVERITY_ERROR = "ERROR" +LOGSEVERITY_CRITICAL = "CRITICAL" +LOGSEVERITY_WARN = "WARN" +LOGSEVERITY_FATAL = "FATAL" + +LOGSEVERITY = { + LOGSEVERITY_CRITICAL: logging.CRITICAL, + LOGSEVERITY_FATAL: logging.FATAL, + LOGSEVERITY_ERROR: logging.ERROR, + LOGSEVERITY_WARNING: logging.WARNING, + LOGSEVERITY_WARN: logging.WARN, + LOGSEVERITY_INFO: logging.INFO, + LOGSEVERITY_DEBUG: logging.DEBUG, + LOGSEVERITY_NOTSET: logging.NOTSET, +} + + +DEFAULT_LOGSEVERITY = "DEBUG" + +LOGGER_DEFAULT = "default" +LOGGER_LOGS = "logs" +LOGGER_FILTERS = "filters" + +ATTR_LEVEL = "level" + +EVENT_LOGGING_CHANGED = "logging_changed" + +STORAGE_KEY = "core.logger" +STORAGE_LOG_KEY = "logs" +STORAGE_VERSION = 1 diff --git a/homeassistant/components/logger/helpers.py b/homeassistant/components/logger/helpers.py new file mode 100644 index 00000000000..d85486a41e0 --- /dev/null +++ b/homeassistant/components/logger/helpers.py @@ -0,0 +1,217 @@ +"""Helpers for the logger integration.""" +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Mapping +import contextlib +from dataclasses import asdict, dataclass +import logging +from typing import Any, cast + +from homeassistant.backports.enum import StrEnum +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import IntegrationNotFound, async_get_integration + +from .const import ( + DOMAIN, + EVENT_LOGGING_CHANGED, + LOGGER_DEFAULT, + LOGGER_LOGS, + LOGSEVERITY, + LOGSEVERITY_NOTSET, + STORAGE_KEY, + STORAGE_LOG_KEY, + STORAGE_VERSION, +) + + +@callback +def async_get_domain_config(hass: HomeAssistant) -> LoggerDomainConfig: + """Return the domain config.""" + return cast(LoggerDomainConfig, hass.data[DOMAIN]) + + +@callback +def set_default_log_level(hass: HomeAssistant, level: int) -> None: + """Set the default log level for components.""" + _set_log_level(logging.getLogger(""), level) + hass.bus.async_fire(EVENT_LOGGING_CHANGED) + + +@callback +def set_log_levels(hass: HomeAssistant, logpoints: Mapping[str, int]) -> None: + """Set the specified log levels.""" + async_get_domain_config(hass).overrides.update(logpoints) + for key, value in logpoints.items(): + _set_log_level(logging.getLogger(key), value) + hass.bus.async_fire(EVENT_LOGGING_CHANGED) + + +def _set_log_level(logger: logging.Logger, level: int) -> None: + """Set the log level. + + Any logger fetched before this integration is loaded will use old class. + """ + getattr(logger, "orig_setLevel", logger.setLevel)(level) + + +def _chattiest_log_level(level1: int, level2: int) -> int: + """Return the chattiest log level.""" + if level1 == logging.NOTSET: + return level2 + if level2 == logging.NOTSET: + return level1 + return min(level1, level2) + + +async def get_integration_loggers(hass: HomeAssistant, domain: str) -> list[str]: + """Get loggers for an integration.""" + loggers = [f"homeassistant.components.{domain}"] + with contextlib.suppress(IntegrationNotFound): + integration = await async_get_integration(hass, domain) + if integration.loggers: + loggers.extend(integration.loggers) + return loggers + + +@dataclass +class LoggerSetting: + """Settings for a single module or integration.""" + + level: str + persistence: str + type: str + + +@dataclass +class LoggerDomainConfig: + """Logger domain config.""" + + overrides: dict[str, Any] + settings: LoggerSettings + + +class LogPersistance(StrEnum): + """Log persistence.""" + + NONE = "none" + ONCE = "once" + PERMANENT = "permanent" + + +class LogSettingsType(StrEnum): + """Log settings type.""" + + INTEGRATION = "integration" + MODULE = "module" + + +class LoggerSettings: + """Manage log settings.""" + + _stored_config: dict[str, dict[str, LoggerSetting]] + + def __init__(self, hass: HomeAssistant, yaml_config: ConfigType) -> None: + """Initialize log settings.""" + + self._yaml_config = yaml_config + self._default_level = logging.INFO + if DOMAIN in yaml_config: + self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT] + self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store( + hass, STORAGE_VERSION, STORAGE_KEY + ) + + async def async_load(self) -> None: + """Load stored settings.""" + stored_config = await self._store.async_load() + if not stored_config: + self._stored_config = {STORAGE_LOG_KEY: {}} + return + + def reset_persistence(settings: LoggerSetting) -> LoggerSetting: + """Reset persistence.""" + if settings.persistence == LogPersistance.ONCE: + settings.persistence = LogPersistance.NONE + return settings + + stored_log_config = stored_config[STORAGE_LOG_KEY] + # Reset domains for which the overrides should only be applied once + self._stored_config = { + STORAGE_LOG_KEY: { + domain: reset_persistence(LoggerSetting(**settings)) + for domain, settings in stored_log_config.items() + } + } + await self._store.async_save(self._async_data_to_save()) + + @callback + def _async_data_to_save(self) -> dict[str, dict[str, dict[str, str]]]: + """Generate data to be saved.""" + stored_log_config = self._stored_config[STORAGE_LOG_KEY] + return { + STORAGE_LOG_KEY: { + domain: asdict(settings) + for domain, settings in stored_log_config.items() + if settings.persistence + in (LogPersistance.ONCE, LogPersistance.PERMANENT) + } + } + + @callback + def async_save(self) -> None: + """Save settings.""" + self._store.async_delay_save(self._async_data_to_save, 15) + + @callback + def _async_get_logger_logs(self) -> dict[str, int]: + """Get the logger logs.""" + logger_logs: dict[str, int] = self._yaml_config.get(DOMAIN, {}).get( + LOGGER_LOGS, {} + ) + return logger_logs + + async def async_update( + self, hass: HomeAssistant, domain: str, settings: LoggerSetting + ) -> None: + """Update settings.""" + stored_log_config = self._stored_config[STORAGE_LOG_KEY] + if settings.level == LOGSEVERITY_NOTSET: + stored_log_config.pop(domain, None) + else: + stored_log_config[domain] = settings + + self.async_save() + + if settings.type == LogSettingsType.INTEGRATION: + loggers = await get_integration_loggers(hass, domain) + else: + loggers = [domain] + + combined_logs = {logger: LOGSEVERITY[settings.level] for logger in loggers} + # Don't override the log levels with the ones from YAML + # since we want whatever the user is asking for to be honored. + + set_log_levels(hass, combined_logs) + + async def async_get_levels(self, hass: HomeAssistant) -> dict[str, int]: + """Get combination of levels from yaml and storage.""" + combined_logs = defaultdict(lambda: logging.CRITICAL) + for domain, settings in self._stored_config[STORAGE_LOG_KEY].items(): + if settings.type == LogSettingsType.INTEGRATION: + loggers = await get_integration_loggers(hass, domain) + else: + loggers = [domain] + + for logger in loggers: + combined_logs[logger] = LOGSEVERITY[settings.level] + + if yaml_log_settings := self._async_get_logger_logs(): + for domain, level in yaml_log_settings.items(): + combined_logs[domain] = _chattiest_log_level( + combined_logs[domain], level + ) + + return dict(combined_logs) diff --git a/homeassistant/components/logger/websocket_api.py b/homeassistant/components/logger/websocket_api.py new file mode 100644 index 00000000000..1b4e5cb36a6 --- /dev/null +++ b/homeassistant/components/logger/websocket_api.py @@ -0,0 +1,104 @@ +"""Websocket API handlers for the logger integration.""" +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.setup import async_get_loaded_integrations + +from .const import LOGSEVERITY +from .helpers import ( + LoggerSetting, + LogPersistance, + LogSettingsType, + async_get_domain_config, +) + + +@callback +def async_load_websocket_api(hass: HomeAssistant) -> None: + """Set up the websocket API.""" + websocket_api.async_register_command(hass, handle_integration_log_info) + websocket_api.async_register_command(hass, handle_integration_log_level) + websocket_api.async_register_command(hass, handle_module_log_level) + + +@websocket_api.websocket_command({vol.Required("type"): "logger/log_info"}) +@websocket_api.async_response +async def handle_integration_log_info( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle integrations logger info.""" + connection.send_result( + msg["id"], + [ + { + "domain": integration, + "level": logging.getLogger( + f"homeassistant.components.{integration}" + ).getEffectiveLevel(), + } + for integration in async_get_loaded_integrations(hass) + ], + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "logger/integration_log_level", + vol.Required("integration"): str, + vol.Required("level"): vol.In(LOGSEVERITY), + vol.Required("persistence"): vol.Coerce(LogPersistance), + } +) +@websocket_api.async_response +async def handle_integration_log_level( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle setting integration log level.""" + try: + await async_get_integration(hass, msg["integration"]) + except IntegrationNotFound: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Integration not found" + ) + return + await async_get_domain_config(hass).settings.async_update( + hass, + msg["integration"], + LoggerSetting( + level=msg["level"], + persistence=msg["persistence"], + type=LogSettingsType.INTEGRATION, + ), + ) + connection.send_message(websocket_api.messages.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "logger/log_level", + vol.Required("module"): str, + vol.Required("level"): vol.In(LOGSEVERITY), + vol.Required("persistence"): vol.Coerce(LogPersistance), + } +) +@websocket_api.async_response +async def handle_module_log_level( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle setting integration log level.""" + await async_get_domain_config(hass).settings.async_update( + hass, + msg["module"], + LoggerSetting( + level=msg["level"], + persistence=msg["persistence"], + type=LogSettingsType.MODULE, + ), + ) + connection.send_message(websocket_api.messages.result_message(msg["id"])) diff --git a/mypy.ini b/mypy.ini index 326bbc5ed36..fa245821ede 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1493,6 +1493,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.logger.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lookin.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/logger/conftest.py b/tests/components/logger/conftest.py new file mode 100644 index 00000000000..00d27753a61 --- /dev/null +++ b/tests/components/logger/conftest.py @@ -0,0 +1,12 @@ +"""Test fixtures for the Logger component.""" +import logging + +import pytest + + +@pytest.fixture(autouse=True) +def restore_logging_class(): + """Restore logging class.""" + klass = logging.getLoggerClass() + yield + logging.setLoggerClass(klass) diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 6435ef95394..6b6269c4099 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -3,8 +3,6 @@ from collections import defaultdict import logging from unittest.mock import Mock, patch -import pytest - from homeassistant.components import logger from homeassistant.components.logger import LOGSEVERITY from homeassistant.setup import async_setup_component @@ -15,14 +13,8 @@ ZONE_NS = f"{COMPONENTS_NS}.zone" GROUP_NS = f"{COMPONENTS_NS}.group" CONFIGED_NS = "otherlibx" UNCONFIG_NS = "unconfigurednamespace" - - -@pytest.fixture(autouse=True) -def restore_logging_class(): - """Restore logging class.""" - klass = logging.getLoggerClass() - yield - logging.setLoggerClass(klass) +INTEGRATION = "test_component" +INTEGRATION_NS = f"homeassistant.components.{INTEGRATION}" async def test_log_filtering(hass, caplog): @@ -158,7 +150,7 @@ async def test_setting_level(hass): ) -async def test_can_set_level(hass): +async def test_can_set_level_from_yaml(hass): """Test logger propagation.""" assert await async_setup_component( @@ -178,7 +170,49 @@ async def test_can_set_level(hass): } }, ) + await _assert_log_levels(hass) + _reset_logging() + +async def test_can_set_level_from_store(hass, hass_storage): + """Test setting up logs from store.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + CONFIGED_NS: { + "level": "WARNING", + "persistence": "once", + "type": "module", + }, + f"{CONFIGED_NS}.info": { + "level": "INFO", + "persistence": "once", + "type": "module", + }, + f"{CONFIGED_NS}.debug": { + "level": "DEBUG", + "persistence": "once", + "type": "module", + }, + HASS_NS: {"level": "WARNING", "persistence": "once", "type": "module"}, + COMPONENTS_NS: { + "level": "INFO", + "persistence": "once", + "type": "module", + }, + ZONE_NS: {"level": "DEBUG", "persistence": "once", "type": "module"}, + GROUP_NS: {"level": "INFO", "persistence": "once", "type": "module"}, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component(hass, "logger", {}) + await _assert_log_levels(hass) + _reset_logging() + + +async def _assert_log_levels(hass): assert logging.getLogger(UNCONFIG_NS).level == logging.NOTSET assert logging.getLogger(UNCONFIG_NS).isEnabledFor(logging.CRITICAL) is True assert ( @@ -255,3 +289,113 @@ async def test_can_set_level(hass): assert logging.getLogger(CONFIGED_NS).level == logging.WARNING logging.getLogger("").setLevel(logging.NOTSET) + + +def _reset_logging(): + """Reset loggers.""" + logging.getLogger(CONFIGED_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(f"{CONFIGED_NS}.info").orig_setLevel(logging.NOTSET) + logging.getLogger(f"{CONFIGED_NS}.debug").orig_setLevel(logging.NOTSET) + logging.getLogger(HASS_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(COMPONENTS_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(ZONE_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(GROUP_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(INTEGRATION_NS).orig_setLevel(logging.NOTSET) + + +async def test_can_set_integration_level_from_store(hass, hass_storage): + """Test setting up integration logs from store.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + INTEGRATION: { + "level": "WARNING", + "persistence": "once", + "type": "integration", + }, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component(hass, "logger", {}) + + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.DEBUG) is False + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.WARNING) is True + + _reset_logging() + + +async def test_chattier_log_level_wins_1(hass, hass_storage): + """Test chattier log level in store takes precedence.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + INTEGRATION_NS: { + "level": "DEBUG", + "persistence": "once", + "type": "module", + }, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component( + hass, + "logger", + { + "logger": { + "logs": { + INTEGRATION_NS: "warning", + } + } + }, + ) + + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.DEBUG) is True + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.WARNING) is True + + _reset_logging() + + +async def test_chattier_log_level_wins_2(hass, hass_storage): + """Test chattier log level in yaml takes precedence.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + INTEGRATION_NS: { + "level": "WARNING", + "persistence": "once", + "type": "module", + }, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component( + hass, "logger", {"logger": {"logs": {INTEGRATION_NS: "debug"}}} + ) + + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.DEBUG) is True + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.WARNING) is True + + _reset_logging() + + +async def test_log_once_removed_from_store(hass, hass_storage): + """Test logs with persistence "once" are removed from the store at startup.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + ZONE_NS: {"type": "module", "level": "DEBUG", "persistence": "once"} + } + }, + "key": "core.logger", + "version": 1, + } + + assert await async_setup_component(hass, "logger", {}) + + assert hass_storage["core.logger"]["data"] == {"logs": {}} diff --git a/tests/components/logger/test_websocket_api.py b/tests/components/logger/test_websocket_api.py new file mode 100644 index 00000000000..1448196f1a3 --- /dev/null +++ b/tests/components/logger/test_websocket_api.py @@ -0,0 +1,195 @@ +"""Tests for Logger Websocket API commands.""" +import logging + +from homeassistant.components.logger.helpers import async_get_domain_config +from homeassistant.components.websocket_api import const +from homeassistant.setup import async_setup_component + + +async def test_integration_log_info(hass, hass_ws_client, hass_admin_user): + """Test fetching integration log info.""" + + assert await async_setup_component(hass, "logger", {}) + + logging.getLogger("homeassistant.components.http").setLevel(logging.DEBUG) + logging.getLogger("homeassistant.components.websocket_api").setLevel(logging.DEBUG) + + websocket_client = await hass_ws_client() + await websocket_client.send_json({"id": 7, "type": "logger/log_info"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert {"domain": "http", "level": logging.DEBUG} in msg["result"] + assert {"domain": "websocket_api", "level": logging.DEBUG} in msg["result"] + + +async def test_integration_log_level_logger_not_loaded( + hass, hass_ws_client, hass_admin_user +): + """Test setting integration log level.""" + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logger/log_level", + "integration": "websocket_api", + "level": logging.DEBUG, + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + + +async def test_integration_log_level(hass, hass_ws_client, hass_admin_user): + """Test setting integration log level.""" + websocket_client = await hass_ws_client() + assert await async_setup_component(hass, "logger", {}) + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/integration_log_level", + "integration": "websocket_api", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.DEBUG + } + + +async def test_integration_log_level_unknown_integration( + hass, hass_ws_client, hass_admin_user +): + """Test setting integration log level for an unknown integration.""" + websocket_client = await hass_ws_client() + assert await async_setup_component(hass, "logger", {}) + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/integration_log_level", + "integration": "websocket_api_123", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + + +async def test_module_log_level(hass, hass_ws_client, hass_admin_user): + """Test setting integration log level.""" + websocket_client = await hass_ws_client() + assert await async_setup_component( + hass, + "logger", + {"logger": {"logs": {"homeassistant.components.other_component": "warning"}}}, + ) + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.DEBUG, + "homeassistant.components.other_component": logging.WARNING, + } + + +async def test_module_log_level_override(hass, hass_ws_client, hass_admin_user): + """Test override yaml integration log level.""" + websocket_client = await hass_ws_client() + assert await async_setup_component( + hass, + "logger", + {"logger": {"logs": {"homeassistant.components.websocket_api": "warning"}}}, + ) + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.WARNING + } + + await websocket_client.send_json( + { + "id": 6, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "ERROR", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.ERROR + } + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.DEBUG + } + + await websocket_client.send_json( + { + "id": 8, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "NOTSET", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 8 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.NOTSET + }