Home Assistant Unofficial Reference 2024.12.1
logging.py
Go to the documentation of this file.
1 """Logging utilities."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Coroutine
6 from functools import partial, wraps
7 import inspect
8 import logging
9 import logging.handlers
10 import queue
11 import traceback
12 from typing import Any, cast, overload
13 
14 from homeassistant.core import (
15  HassJobType,
16  HomeAssistant,
17  callback,
18  get_hassjob_callable_job_type,
19 )
20 
21 
22 class HomeAssistantQueueHandler(logging.handlers.QueueHandler):
23  """Process the log in another thread."""
24 
25  listener: logging.handlers.QueueListener | None = None
26 
27  def handle(self, record: logging.LogRecord) -> Any:
28  """Conditionally emit the specified logging record.
29 
30  Depending on which filters have been added to the handler, push the new
31  records onto the backing Queue.
32 
33  The default python logger Handler acquires a lock
34  in the parent class which we do not need as
35  SimpleQueue is already thread safe.
36 
37  See https://bugs.python.org/issue24645
38  """
39  return_value = self.filter(record)
40  if return_value:
41  self.emit(record)
42  return return_value
43 
44  def close(self) -> None:
45  """Tidy up any resources used by the handler.
46 
47  This adds shutdown of the QueueListener
48  """
49  super().close()
50  if not self.listenerlistener:
51  return
52  self.listenerlistener.stop()
53  self.listenerlistener = None
54 
55 
56 @callback
57 def async_activate_log_queue_handler(hass: HomeAssistant) -> None:
58  """Migrate the existing log handlers to use the queue.
59 
60  This allows us to avoid blocking I/O and formatting messages
61  in the event loop as log messages are written in another thread.
62  """
63  simple_queue: queue.SimpleQueue[logging.Handler] = queue.SimpleQueue()
64  queue_handler = HomeAssistantQueueHandler(simple_queue)
65  logging.root.addHandler(queue_handler)
66 
67  migrated_handlers: list[logging.Handler] = []
68  for handler in logging.root.handlers[:]:
69  if handler is queue_handler:
70  continue
71  logging.root.removeHandler(handler)
72  migrated_handlers.append(handler)
73 
74  listener = logging.handlers.QueueListener(simple_queue, *migrated_handlers)
75  queue_handler.listener = listener
76 
77  listener.start()
78 
79 
80 def log_exception[*_Ts](format_err: Callable[[*_Ts], Any], *args: *_Ts) -> None:
81  """Log an exception with additional context."""
82  module = inspect.getmodule(inspect.stack(context=0)[1].frame)
83  if module is not None:
84  module_name = module.__name__
85  else:
86  # If Python is unable to access the sources files, the call stack frame
87  # will be missing information, so let's guard.
88  # https://github.com/home-assistant/core/issues/24982
89  module_name = __name__
90 
91  # Do not print the wrapper in the traceback
92  frames = len(inspect.trace()) - 1
93  exc_msg = traceback.format_exc(-frames)
94  friendly_msg = format_err(*args)
95  logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg)
96 
97 
98 async def _async_wrapper[*_Ts](
99  async_func: Callable[[*_Ts], Coroutine[Any, Any, None]],
100  format_err: Callable[[*_Ts], Any],
101  *args: *_Ts,
102 ) -> None:
103  """Catch and log exception."""
104  try:
105  await async_func(*args)
106  except Exception: # noqa: BLE001
107  log_exception(format_err, *args)
108 
109 
110 def _sync_wrapper[*_Ts](
111  func: Callable[[*_Ts], Any], format_err: Callable[[*_Ts], Any], *args: *_Ts
112 ) -> None:
113  """Catch and log exception."""
114  try:
115  func(*args)
116  except Exception: # noqa: BLE001
117  log_exception(format_err, *args)
118 
119 
120 @callback
121 def _callback_wrapper[*_Ts](
122  func: Callable[[*_Ts], Any], format_err: Callable[[*_Ts], Any], *args: *_Ts
123 ) -> None:
124  """Catch and log exception."""
125  try:
126  func(*args)
127  except Exception: # noqa: BLE001
128  log_exception(format_err, *args)
129 
130 
131 @overload
132 def catch_log_exception[*_Ts](
133  func: Callable[[*_Ts], Coroutine[Any, Any, Any]],
134  format_err: Callable[[*_Ts], Any],
135  job_type: HassJobType | None = None,
136 ) -> Callable[[*_Ts], Coroutine[Any, Any, None]]: ...
137 
138 
139 @overload
140 def catch_log_exception[*_Ts](
141  func: Callable[[*_Ts], Any],
142  format_err: Callable[[*_Ts], Any],
143  job_type: HassJobType | None = None,
144 ) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]: ...
145 
146 
147 def catch_log_exception[*_Ts](
148  func: Callable[[*_Ts], Any],
149  format_err: Callable[[*_Ts], Any],
150  job_type: HassJobType | None = None,
151 ) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]:
152  """Decorate a function func to catch and log exceptions.
153 
154  If func is a coroutine function, a coroutine function will be returned.
155  If func is a callback, a callback will be returned.
156  """
157  if job_type is None:
158  job_type = get_hassjob_callable_job_type(func)
159 
160  if job_type is HassJobType.Coroutinefunction:
161  async_func = cast(Callable[[*_Ts], Coroutine[Any, Any, None]], func)
162  return wraps(async_func)(partial(_async_wrapper, async_func, format_err)) # type: ignore[return-value]
163 
164  if job_type is HassJobType.Callback:
165  return wraps(func)(partial(_callback_wrapper, func, format_err)) # type: ignore[return-value]
166 
167  return wraps(func)(partial(_sync_wrapper, func, format_err)) # type: ignore[return-value]
168 
169 
170 def catch_log_coro_exception[_T, *_Ts](
171  target: Coroutine[Any, Any, _T], format_err: Callable[[*_Ts], Any], *args: *_Ts
172 ) -> Coroutine[Any, Any, _T | None]:
173  """Decorate a coroutine to catch and log exceptions."""
174 
175  async def coro_wrapper(*args: *_Ts) -> _T | None:
176  """Catch and log exception."""
177  try:
178  return await target
179  except Exception: # noqa: BLE001
180  log_exception(format_err, *args)
181  return None
182 
183  return coro_wrapper(*args)
184 
185 
186 def async_create_catching_coro[_T](
187  target: Coroutine[Any, Any, _T],
188 ) -> Coroutine[Any, Any, _T | None]:
189  """Wrap a coroutine to catch and log exceptions.
190 
191  The exception will be logged together with a stacktrace of where the
192  coroutine was wrapped.
193 
194  target: target coroutine.
195  """
196  trace = traceback.extract_stack()
197  return catch_log_coro_exception(
198  target,
199  lambda: (
200  f"Exception in {target.__name__} called from\n"
201  + "".join(traceback.format_list(trace[:-1]))
202  ),
203  )
Any handle(self, logging.LogRecord record)
Definition: logging.py:27
HassJobType get_hassjob_callable_job_type(Callable[..., Any] target)
Definition: core.py:387
None async_activate_log_queue_handler(HomeAssistant hass)
Definition: logging.py:57