core/homeassistant/components/logger/helpers.py

218 lines
6.7 KiB
Python

"""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)