1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-04 21:25:26 +01:00
Files
core/homeassistant/components/trace/util.py
T
2026-06-22 10:54:16 +02:00

148 lines
5.1 KiB
Python

"""Support for script and automation tracing and debugging."""
from collections.abc import Mapping
import logging
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.limited_size_dict import LimitedSizeDict
from .const import DATA_TRACE, DATA_TRACE_STORE, DATA_TRACES_RESTORED
from .models import ActionTrace, BaseTrace, RestoredTrace, TraceBuckets, TraceData
_LOGGER = logging.getLogger(__name__)
async def async_get_trace(
hass: HomeAssistant, key: str, run_id: str
) -> dict[str, BaseTrace]:
"""Return the requested trace."""
# Restore saved traces if not done
await async_restore_traces(hass)
trace_bucket = hass.data[DATA_TRACE][key]
trace = trace_bucket.runs.get(run_id) or trace_bucket.not_triggered[run_id]
return trace.as_extended_dict()
async def async_list_contexts(
hass: HomeAssistant, key: str | None
) -> dict[str, dict[str, str]]:
"""List contexts for which we have traces."""
# Restore saved traces if not done
await async_restore_traces(hass)
values: Mapping[str, TraceBuckets | None] | TraceData
if key is not None:
values = {key: hass.data[DATA_TRACE].get(key)}
else:
values = hass.data[DATA_TRACE]
def _trace_id(run_id: str, key: str) -> dict[str, str]:
"""Make trace_id for the response."""
domain, item_id = key.split(".", 1)
return {"run_id": run_id, "domain": domain, "item_id": item_id}
return {
trace.context.id: _trace_id(trace.run_id, key)
for key, trace_bucket in values.items()
if trace_bucket is not None
for trace in trace_bucket.all_traces()
}
def _get_debug_traces(hass: HomeAssistant, key: str) -> list[dict[str, Any]]:
"""Return a serializable list of debug traces for a script or automation."""
if trace_bucket := hass.data[DATA_TRACE].get(key):
return [trace.as_short_dict() for trace in trace_bucket.all_traces()]
return []
async def async_list_traces(
hass: HomeAssistant, wanted_domain: str, wanted_key: str | None
) -> list[dict[str, Any]]:
"""List traces for a domain."""
# Restore saved traces if not done already
await async_restore_traces(hass)
if not wanted_key:
traces: list[dict[str, Any]] = []
for key in hass.data[DATA_TRACE]:
domain = key.split(".", 1)[0]
if domain == wanted_domain:
traces.extend(_get_debug_traces(hass, key))
else:
traces = _get_debug_traces(hass, wanted_key)
return traces
def async_store_trace(
hass: HomeAssistant, trace: ActionTrace, stored_traces: int
) -> None:
"""Store a trace if its key is valid.
Run traces and not-triggered traces are kept in separate, independently
size-limited buckets so a flood of not-triggered traces never evicts runs.
"""
if key := trace.key:
traces = hass.data[DATA_TRACE]
if key not in traces:
traces[key] = TraceBuckets(
runs=LimitedSizeDict(size_limit=stored_traces),
not_triggered=LimitedSizeDict(size_limit=stored_traces),
)
trace_bucket = traces[key]
trace_bucket.runs.size_limit = stored_traces
trace_bucket.not_triggered.size_limit = stored_traces
bucket = trace_bucket.bucket(trace.not_triggered)
bucket[trace.run_id] = trace
def _async_store_restored_trace(hass: HomeAssistant, trace: RestoredTrace) -> None:
"""Store a restored trace and move it to the end of the LimitedSizeDict."""
key = trace.key
traces = hass.data[DATA_TRACE]
if key not in traces:
traces[key] = TraceBuckets(
runs=LimitedSizeDict(), not_triggered=LimitedSizeDict()
)
bucket = traces[key].bucket(trace.not_triggered)
bucket[trace.run_id] = trace
bucket.move_to_end(trace.run_id, last=False)
async def async_restore_traces(hass: HomeAssistant) -> None:
"""Restore saved traces."""
if DATA_TRACES_RESTORED in hass.data:
return
hass.data[DATA_TRACES_RESTORED] = True
store = hass.data[DATA_TRACE_STORE]
try:
restored_traces = await store.async_load() or {}
except HomeAssistantError:
_LOGGER.exception("Error loading traces")
restored_traces = {}
for key, traces in restored_traces.items():
# Add stored traces in reversed order to prioritize the newest traces
for json_trace in reversed(traces):
try:
trace = RestoredTrace(json_trace)
# Catch any exception to not blow up if the stored trace is invalid
except Exception:
_LOGGER.exception("Failed to restore trace")
continue
# Runs and not-triggered traces are capped independently, so check
# the bucket this trace belongs to rather than breaking the loop.
if (trace_bucket := hass.data[DATA_TRACE].get(key)) is not None:
bucket = trace_bucket.bucket(trace.not_triggered)
if bucket.size_limit is not None and len(bucket) >= bucket.size_limit:
continue
_async_store_restored_trace(hass, trace)