-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Refactor tracing: Move trace support to its own integration #48224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,26 +1,17 @@ | ||||||||||||
| """Trace support for automation.""" | ||||||||||||
| from __future__ import annotations | ||||||||||||
|
|
||||||||||||
| from collections import OrderedDict | ||||||||||||
| from contextlib import contextmanager | ||||||||||||
| import datetime as dt | ||||||||||||
| from datetime import timedelta | ||||||||||||
| from itertools import count | ||||||||||||
| import logging | ||||||||||||
| from typing import Any, Awaitable, Callable, Deque | ||||||||||||
| from typing import Any, Deque | ||||||||||||
|
|
||||||||||||
| from homeassistant.core import Context, HomeAssistant, callback | ||||||||||||
| from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder | ||||||||||||
| from homeassistant.components.trace.const import DATA_TRACE, STORED_TRACES | ||||||||||||
| from homeassistant.components.trace.utils import LimitedSizeDict | ||||||||||||
| from homeassistant.core import Context | ||||||||||||
| from homeassistant.helpers.trace import TraceElement, trace_id_set | ||||||||||||
| from homeassistant.helpers.typing import TemplateVarsType | ||||||||||||
| from homeassistant.util import dt as dt_util | ||||||||||||
|
|
||||||||||||
| DATA_AUTOMATION_TRACE = "automation_trace" | ||||||||||||
| STORED_TRACES = 5 # Stored traces per automation | ||||||||||||
|
|
||||||||||||
| _LOGGER = logging.getLogger(__name__) | ||||||||||||
| AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] | ||||||||||||
|
|
||||||||||||
| # mypy: allow-untyped-calls, allow-untyped-defs | ||||||||||||
| # mypy: no-check-untyped-defs, no-warn-return-any | ||||||||||||
|
|
||||||||||||
|
|
@@ -134,35 +125,14 @@ def as_short_dict(self) -> dict[str, Any]: | |||||||||||
| return result | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class LimitedSizeDict(OrderedDict): | ||||||||||||
| """OrderedDict limited in size.""" | ||||||||||||
|
|
||||||||||||
| def __init__(self, *args, **kwds): | ||||||||||||
| """Initialize OrderedDict limited in size.""" | ||||||||||||
| self.size_limit = kwds.pop("size_limit", None) | ||||||||||||
| OrderedDict.__init__(self, *args, **kwds) | ||||||||||||
| self._check_size_limit() | ||||||||||||
|
|
||||||||||||
| def __setitem__(self, key, value): | ||||||||||||
| """Set item and check dict size.""" | ||||||||||||
| OrderedDict.__setitem__(self, key, value) | ||||||||||||
| self._check_size_limit() | ||||||||||||
|
|
||||||||||||
| def _check_size_limit(self): | ||||||||||||
| """Check dict size and evict items in FIFO order if needed.""" | ||||||||||||
| if self.size_limit is not None: | ||||||||||||
| while len(self) > self.size_limit: | ||||||||||||
| self.popitem(last=False) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @contextmanager | ||||||||||||
| def trace_automation(hass, unique_id, config, context): | ||||||||||||
| """Trace action execution of automation with automation_id.""" | ||||||||||||
| automation_trace = AutomationTrace(unique_id, config, context) | ||||||||||||
| trace_id_set((unique_id, automation_trace.run_id)) | ||||||||||||
|
|
||||||||||||
| if unique_id: | ||||||||||||
| automation_traces = hass.data[DATA_AUTOMATION_TRACE] | ||||||||||||
| automation_traces = hass.data[DATA_TRACE] | ||||||||||||
| if unique_id not in automation_traces: | ||||||||||||
| automation_traces[unique_id] = LimitedSizeDict(size_limit=STORED_TRACES) | ||||||||||||
| automation_traces[unique_id][automation_trace.run_id] = automation_trace | ||||||||||||
|
Comment on lines
+135
to
138
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should abstract away how traces are stored from
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #48276 |
||||||||||||
|
|
@@ -176,50 +146,3 @@ def trace_automation(hass, unique_id, config, context): | |||||||||||
| finally: | ||||||||||||
| if unique_id: | ||||||||||||
| automation_trace.finished() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @callback | ||||||||||||
| def get_debug_trace(hass, automation_id, run_id): | ||||||||||||
| """Return a serializable debug trace.""" | ||||||||||||
| return hass.data[DATA_AUTOMATION_TRACE][automation_id][run_id] | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @callback | ||||||||||||
| def get_debug_traces_for_automation(hass, automation_id, summary=False): | ||||||||||||
| """Return a serializable list of debug traces for an automation.""" | ||||||||||||
| traces = [] | ||||||||||||
|
|
||||||||||||
| for trace in hass.data[DATA_AUTOMATION_TRACE].get(automation_id, {}).values(): | ||||||||||||
| if summary: | ||||||||||||
| traces.append(trace.as_short_dict()) | ||||||||||||
| else: | ||||||||||||
| traces.append(trace.as_dict()) | ||||||||||||
|
|
||||||||||||
| return traces | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @callback | ||||||||||||
| def get_debug_traces(hass, summary=False): | ||||||||||||
| """Return a serializable list of debug traces.""" | ||||||||||||
| traces = [] | ||||||||||||
|
|
||||||||||||
| for automation_id in hass.data[DATA_AUTOMATION_TRACE]: | ||||||||||||
| traces.extend(get_debug_traces_for_automation(hass, automation_id, summary)) | ||||||||||||
|
|
||||||||||||
| return traces | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class TraceJSONEncoder(HAJSONEncoder): | ||||||||||||
| """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" | ||||||||||||
|
|
||||||||||||
| def default(self, o: Any) -> Any: | ||||||||||||
| """Convert certain objects. | ||||||||||||
|
|
||||||||||||
| Fall back to repr(o). | ||||||||||||
| """ | ||||||||||||
| if isinstance(o, timedelta): | ||||||||||||
| return {"__type": str(type(o)), "total_seconds": o.total_seconds()} | ||||||||||||
| try: | ||||||||||||
| return super().default(o) | ||||||||||||
| except TypeError: | ||||||||||||
| return {"__type": str(type(o)), "repr": repr(o)} | ||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||||
| """Support for automation and script tracing and debugging.""" | ||||||
| from . import websocket_api | ||||||
| from .const import DATA_TRACE | ||||||
|
|
||||||
| DOMAIN = "trace" | ||||||
|
|
||||||
|
|
||||||
| async def async_setup(hass, config): | ||||||
| """Initialize the trace integration.""" | ||||||
| hass.data.setdefault(DATA_TRACE, {}) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #48276 |
||||||
| websocket_api.async_setup(hass) | ||||||
| return True | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,4 @@ | ||||||
| """Shared constants for automation and script tracing and debugging.""" | ||||||
|
|
||||||
| DATA_TRACE = "trace" | ||||||
| STORED_TRACES = 5 # Stored traces per automation | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #48276 |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "domain": "trace", | ||
| "name": "Trace", | ||
| "documentation": "https://www.home-assistant.io/integrations/automation", | ||
| "codeowners": [ | ||
| "@home-assistant/core" | ||
| ], | ||
| "quality_scale": "internal" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """Support for automation and script tracing and debugging.""" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file needs a better name. Any reason we wouldn't just put this in the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #48288 |
||
| from homeassistant.core import callback | ||
|
|
||
| from .const import DATA_TRACE | ||
|
|
||
|
|
||
| @callback | ||
| def get_debug_trace(hass, automation_id, run_id): | ||
| """Return a serializable debug trace.""" | ||
| return hass.data[DATA_TRACE][automation_id][run_id] | ||
|
|
||
|
|
||
| @callback | ||
| def get_debug_traces_for_automation(hass, automation_id, summary=False): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #48288 |
||
| """Return a serializable list of debug traces for an automation.""" | ||
| traces = [] | ||
|
|
||
| for trace in hass.data[DATA_TRACE].get(automation_id, {}).values(): | ||
| if summary: | ||
| traces.append(trace.as_short_dict()) | ||
| else: | ||
| traces.append(trace.as_dict()) | ||
|
|
||
| return traces | ||
|
|
||
|
|
||
| @callback | ||
| def get_debug_traces(hass, summary=False): | ||
| """Return a serializable list of debug traces.""" | ||
| traces = [] | ||
|
|
||
| for automation_id in hass.data[DATA_TRACE]: | ||
| traces.extend(get_debug_traces_for_automation(hass, automation_id, summary)) | ||
|
|
||
| return traces | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| """Helpers for automation and script tracing and debugging.""" | ||
| from collections import OrderedDict | ||
| from datetime import timedelta | ||
| from typing import Any | ||
|
|
||
| from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder | ||
|
|
||
|
|
||
| class LimitedSizeDict(OrderedDict): | ||
| """OrderedDict limited in size.""" | ||
|
|
||
| def __init__(self, *args, **kwds): | ||
| """Initialize OrderedDict limited in size.""" | ||
| self.size_limit = kwds.pop("size_limit", None) | ||
| OrderedDict.__init__(self, *args, **kwds) | ||
| self._check_size_limit() | ||
|
|
||
| def __setitem__(self, key, value): | ||
| """Set item and check dict size.""" | ||
| OrderedDict.__setitem__(self, key, value) | ||
| self._check_size_limit() | ||
|
|
||
| def _check_size_limit(self): | ||
| """Check dict size and evict items in FIFO order if needed.""" | ||
| if self.size_limit is not None: | ||
| while len(self) > self.size_limit: | ||
| self.popitem(last=False) | ||
|
|
||
|
|
||
| class TraceJSONEncoder(HAJSONEncoder): | ||
| """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" | ||
|
|
||
| def default(self, o: Any) -> Any: | ||
| """Convert certain objects. | ||
|
|
||
| Fall back to repr(o). | ||
| """ | ||
| if isinstance(o, timedelta): | ||
| return {"__type": str(type(o)), "total_seconds": o.total_seconds()} | ||
| try: | ||
| return super().default(o) | ||
| except TypeError: | ||
| return {"__type": str(type(o)), "repr": repr(o)} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """The tests for Trace.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that we should allow other integrations to be aware of
hass.data[DATA_TRACE]. We should abstract that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in #48276