Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Helpers for the logger integration."""
2 
3 from __future__ import annotations
4 
5 from collections import defaultdict
6 from collections.abc import Mapping
7 import contextlib
8 from dataclasses import asdict, dataclass
9 from enum import StrEnum
10 from functools import lru_cache
11 import logging
12 from typing import Any, cast
13 
14 from homeassistant.const import EVENT_LOGGING_CHANGED
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.helpers.storage import Store
17 from homeassistant.helpers.typing import ConfigType
18 from homeassistant.loader import IntegrationNotFound, async_get_integration
19 
20 from .const import (
21  DOMAIN,
22  LOGGER_DEFAULT,
23  LOGGER_LOGS,
24  LOGSEVERITY,
25  LOGSEVERITY_NOTSET,
26  STORAGE_KEY,
27  STORAGE_LOG_KEY,
28  STORAGE_VERSION,
29 )
30 
31 SAVE_DELAY = 15.0
32 # At startup, we want to save after a long delay to avoid
33 # saving while the system is still starting up. If the system
34 # for some reason restarts quickly, it will still be written
35 # at the final write event. In most cases we expect startup
36 # to happen in less than 180 seconds, but if it takes longer
37 # it's likely delayed because of remote I/O and not local
38 # I/O so it's fine to save at that point.
39 SAVE_DELAY_LONG = 180.0
40 
41 
42 @callback
43 def async_get_domain_config(hass: HomeAssistant) -> LoggerDomainConfig:
44  """Return the domain config."""
45  return cast(LoggerDomainConfig, hass.data[DOMAIN])
46 
47 
48 @callback
49 def set_default_log_level(hass: HomeAssistant, level: int) -> None:
50  """Set the default log level for components."""
51  _set_log_level(logging.getLogger(""), level)
52  hass.bus.async_fire(EVENT_LOGGING_CHANGED)
53 
54 
55 @callback
56 def set_log_levels(hass: HomeAssistant, logpoints: Mapping[str, int]) -> None:
57  """Set the specified log levels."""
58  async_get_domain_config(hass).overrides.update(logpoints)
59  for key, value in logpoints.items():
60  _set_log_level(logging.getLogger(key), value)
61  hass.bus.async_fire(EVENT_LOGGING_CHANGED)
62 
63 
64 def _set_log_level(logger: logging.Logger, level: int) -> None:
65  """Set the log level.
66 
67  Any logger fetched before this integration is loaded will use old class.
68  """
69  getattr(logger, "orig_setLevel", logger.setLevel)(level)
70 
71 
72 def _chattiest_log_level(level1: int, level2: int) -> int:
73  """Return the chattiest log level."""
74  if level1 == logging.NOTSET:
75  return level2
76  if level2 == logging.NOTSET:
77  return level1
78  return min(level1, level2)
79 
80 
81 async def get_integration_loggers(hass: HomeAssistant, domain: str) -> set[str]:
82  """Get loggers for an integration."""
83  loggers: set[str] = {f"homeassistant.components.{domain}"}
84  with contextlib.suppress(IntegrationNotFound):
85  integration = await async_get_integration(hass, domain)
86  loggers.add(integration.pkg_path)
87  if integration.loggers:
88  loggers.update(integration.loggers)
89  return loggers
90 
91 
92 @dataclass(slots=True)
94  """Settings for a single module or integration."""
95 
96  level: str
97  persistence: str
98  type: str
99 
100 
101 @dataclass(slots=True)
103  """Logger domain config."""
104 
105  overrides: dict[str, Any]
106  settings: LoggerSettings
107 
108 
109 class LogPersistance(StrEnum):
110  """Log persistence."""
111 
112  NONE = "none"
113  ONCE = "once"
114  PERMANENT = "permanent"
115 
116 
117 class LogSettingsType(StrEnum):
118  """Log settings type."""
119 
120  INTEGRATION = "integration"
121  MODULE = "module"
122 
123 
125  """Manage log settings."""
126 
127  _stored_config: dict[str, dict[str, LoggerSetting]]
128 
129  def __init__(self, hass: HomeAssistant, yaml_config: ConfigType) -> None:
130  """Initialize log settings."""
131 
132  self._yaml_config_yaml_config = yaml_config
133  self._default_level_default_level = logging.INFO
134  if DOMAIN in yaml_config and LOGGER_DEFAULT in yaml_config[DOMAIN]:
135  self._default_level_default_level = yaml_config[DOMAIN][LOGGER_DEFAULT]
136  self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
137  hass, STORAGE_VERSION, STORAGE_KEY
138  )
139 
140  async def async_load(self) -> None:
141  """Load stored settings."""
142  stored_config = await self._store.async_load()
143  if not stored_config:
144  self._stored_config_stored_config = {STORAGE_LOG_KEY: {}}
145  return
146 
147  def reset_persistence(settings: LoggerSetting) -> LoggerSetting:
148  """Reset persistence."""
149  if settings.persistence == LogPersistance.ONCE:
150  settings.persistence = LogPersistance.NONE
151  return settings
152 
153  stored_log_config = stored_config[STORAGE_LOG_KEY]
154  # Reset domains for which the overrides should only be applied once
155  self._stored_config_stored_config = {
156  STORAGE_LOG_KEY: {
157  domain: reset_persistence(LoggerSetting(**settings))
158  for domain, settings in stored_log_config.items()
159  }
160  }
161  self.async_saveasync_save(SAVE_DELAY_LONG)
162 
163  @callback
164  def _async_data_to_save(self) -> dict[str, dict[str, dict[str, str]]]:
165  """Generate data to be saved."""
166  stored_log_config = self._stored_config_stored_config[STORAGE_LOG_KEY]
167  return {
168  STORAGE_LOG_KEY: {
169  domain: asdict(settings)
170  for domain, settings in stored_log_config.items()
171  if settings.persistence
172  in (LogPersistance.ONCE, LogPersistance.PERMANENT)
173  }
174  }
175 
176  @callback
177  def async_save(self, delay: float = SAVE_DELAY) -> None:
178  """Save settings."""
179  self._store.async_delay_save(self._async_data_to_save_async_data_to_save, delay)
180 
181  @callback
182  def _async_get_logger_logs(self) -> dict[str, int]:
183  """Get the logger logs."""
184  logger_logs: dict[str, int] = self._yaml_config_yaml_config.get(DOMAIN, {}).get(
185  LOGGER_LOGS, {}
186  )
187  return logger_logs
188 
189  async def async_update(
190  self, hass: HomeAssistant, domain: str, settings: LoggerSetting
191  ) -> None:
192  """Update settings."""
193  stored_log_config = self._stored_config_stored_config[STORAGE_LOG_KEY]
194  if settings.level == LOGSEVERITY_NOTSET:
195  stored_log_config.pop(domain, None)
196  else:
197  stored_log_config[domain] = settings
198 
199  self.async_saveasync_save()
200 
201  if settings.type == LogSettingsType.INTEGRATION:
202  loggers = await get_integration_loggers(hass, domain)
203  else:
204  loggers = {domain}
205 
206  combined_logs = {logger: LOGSEVERITY[settings.level] for logger in loggers}
207  # Don't override the log levels with the ones from YAML
208  # since we want whatever the user is asking for to be honored.
209 
210  set_log_levels(hass, combined_logs)
211 
212  async def async_get_levels(self, hass: HomeAssistant) -> dict[str, int]:
213  """Get combination of levels from yaml and storage."""
214  combined_logs = defaultdict(lambda: logging.CRITICAL)
215  for domain, settings in self._stored_config_stored_config[STORAGE_LOG_KEY].items():
216  if settings.type == LogSettingsType.INTEGRATION:
217  loggers = await get_integration_loggers(hass, domain)
218  else:
219  loggers = {domain}
220 
221  for logger in loggers:
222  combined_logs[logger] = LOGSEVERITY[settings.level]
223 
224  if yaml_log_settings := self._async_get_logger_logs_async_get_logger_logs():
225  for domain, level in yaml_log_settings.items():
226  combined_logs[domain] = _chattiest_log_level(
227  combined_logs[domain], level
228  )
229 
230  return dict(combined_logs)
231 
232 
233 get_logger = lru_cache(maxsize=256)(logging.getLogger)
234 """Get a logger.
235 
236 getLogger uses a threading.RLock, so we cache the result to avoid
237 locking the threads every time the integrations page is loaded.
238 """
dict[str, int] async_get_levels(self, HomeAssistant hass)
Definition: helpers.py:212
None async_update(self, HomeAssistant hass, str domain, LoggerSetting settings)
Definition: helpers.py:191
dict[str, dict[str, dict[str, str]]] _async_data_to_save(self)
Definition: helpers.py:164
None async_save(self, float delay=SAVE_DELAY)
Definition: helpers.py:177
None __init__(self, HomeAssistant hass, ConfigType yaml_config)
Definition: helpers.py:129
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None set_log_levels(HomeAssistant hass, Mapping[str, int] logpoints)
Definition: helpers.py:56
LoggerDomainConfig async_get_domain_config(HomeAssistant hass)
Definition: helpers.py:43
int _chattiest_log_level(int level1, int level2)
Definition: helpers.py:72
set[str] get_integration_loggers(HomeAssistant hass, str domain)
Definition: helpers.py:81
None _set_log_level(logging.Logger logger, int level)
Definition: helpers.py:64
None set_default_log_level(HomeAssistant hass, int level)
Definition: helpers.py:49
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
Definition: storage.py:444
Integration async_get_integration(HomeAssistant hass, str domain)
Definition: loader.py:1354