Add websocket endpoints to control integration logging (#65158)
Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
parent
9d607c8bd5
commit
8792d664e7
9 changed files with 784 additions and 69 deletions
|
@ -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.*
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
42
homeassistant/components/logger/const.py
Normal file
42
homeassistant/components/logger/const.py
Normal file
|
@ -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
|
217
homeassistant/components/logger/helpers.py
Normal file
217
homeassistant/components/logger/helpers.py
Normal file
|
@ -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)
|
104
homeassistant/components/logger/websocket_api.py
Normal file
104
homeassistant/components/logger/websocket_api.py
Normal file
|
@ -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"]))
|
10
mypy.ini
10
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
|
||||
|
|
12
tests/components/logger/conftest.py
Normal file
12
tests/components/logger/conftest.py
Normal file
|
@ -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)
|
|
@ -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": {}}
|
||||
|
|
195
tests/components/logger/test_websocket_api.py
Normal file
195
tests/components/logger/test_websocket_api.py
Normal file
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue