Refactor tracing: Move trace support to its own integration (#48224)

pull/48231/head
Erik Montnemery 2021-03-22 19:19:38 +01:00 committed by GitHub
parent 781084880b
commit a49989241a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 120 additions and 98 deletions

View File

@ -490,6 +490,7 @@ homeassistant/components/toon/* @frenck
homeassistant/components/totalconnect/* @austinmroczek
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
homeassistant/components/traccar/* @ludeeus
homeassistant/components/trace/* @home-assistant/core
homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/trafikverket_weatherstation/* @endor-force
homeassistant/components/transmission/* @engrbm87 @JPHutchins

View File

@ -61,7 +61,6 @@ from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.loader import bind_hass
from homeassistant.util.dt import parse_datetime
from . import websocket_api
from .config import AutomationConfig, async_validate_config_item
# Not used except by packages to check config structure
@ -76,7 +75,7 @@ from .const import (
LOGGER,
)
from .helpers import async_get_blueprints
from .trace import DATA_AUTOMATION_TRACE, trace_automation
from .trace import trace_automation
# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
@ -176,9 +175,6 @@ async def async_setup(hass, config):
"""Set up all automations."""
# Local import to avoid circular import
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
hass.data.setdefault(DATA_AUTOMATION_TRACE, {})
websocket_api.async_setup(hass)
# To register the automation blueprints
async_get_blueprints(hass)

View File

@ -2,7 +2,7 @@
"domain": "automation",
"name": "Automation",
"documentation": "https://www.home-assistant.io/integrations/automation",
"dependencies": ["blueprint"],
"dependencies": ["blueprint", "trace"],
"after_dependencies": [
"device_automation",
"webhook"

View File

@ -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,27 +125,6 @@ class AutomationTrace:
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."""
@ -162,7 +132,7 @@ def trace_automation(hass, 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
@ -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)}

View File

@ -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, {})
websocket_api.async_setup(hass)
return True

View File

@ -0,0 +1,4 @@
"""Shared constants for automation and script tracing and debugging."""
DATA_TRACE = "trace"
STORED_TRACES = 5 # Stored traces per automation

View File

@ -0,0 +1,9 @@
{
"domain": "trace",
"name": "Trace",
"documentation": "https://www.home-assistant.io/integrations/automation",
"codeowners": [
"@home-assistant/core"
],
"quality_scale": "internal"
}

View File

@ -0,0 +1,35 @@
"""Support for automation and script tracing and debugging."""
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):
"""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

View File

@ -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)}

View File

@ -24,12 +24,12 @@ from homeassistant.helpers.script import (
)
from .trace import (
DATA_AUTOMATION_TRACE,
TraceJSONEncoder,
DATA_TRACE,
get_debug_trace,
get_debug_traces,
get_debug_traces_for_automation,
)
from .utils import TraceJSONEncoder
# mypy: allow-untyped-calls, allow-untyped-defs
@ -101,11 +101,9 @@ def websocket_automation_trace_contexts(hass, connection, msg):
automation_id = msg.get("automation_id")
if automation_id is not None:
values = {
automation_id: hass.data[DATA_AUTOMATION_TRACE].get(automation_id, {})
}
values = {automation_id: hass.data[DATA_TRACE].get(automation_id, {})}
else:
values = hass.data[DATA_AUTOMATION_TRACE]
values = hass.data[DATA_TRACE]
contexts = {
trace.context.id: {"run_id": trace.run_id, "automation_id": automation_id}

View File

@ -0,0 +1 @@
"""The tests for Trace."""

View File

@ -1,14 +1,14 @@
"""Test Automation trace helpers."""
"""Test trace helpers."""
from datetime import timedelta
from homeassistant import core
from homeassistant.components import automation
from homeassistant.components import trace
from homeassistant.util import dt as dt_util
def test_json_encoder(hass):
"""Test the Trace JSON Encoder."""
ha_json_enc = automation.trace.TraceJSONEncoder()
ha_json_enc = trace.utils.TraceJSONEncoder()
state = core.State("test.test", "hello")
# Test serializing a datetime

View File

@ -1,4 +1,4 @@
"""Test Automation config panel."""
"""Test Trace websocket API."""
from unittest.mock import patch
from homeassistant.bootstrap import async_setup_component