mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
Improve performance of system_log traceback handling (#114992)
This commit is contained in:
parent
90bbfdd53c
commit
657bc969a3
@ -7,6 +7,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from types import FrameType
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -18,7 +19,7 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
KeyType = tuple[str, tuple[str, int], str | None]
|
KeyType = tuple[str, tuple[str, int], tuple[str, int, str] | None]
|
||||||
|
|
||||||
CONF_MAX_ENTRIES = "max_entries"
|
CONF_MAX_ENTRIES = "max_entries"
|
||||||
CONF_FIRE_EVENT = "fire_event"
|
CONF_FIRE_EVENT = "fire_event"
|
||||||
@ -65,16 +66,18 @@ SERVICE_WRITE_SCHEMA = vol.Schema(
|
|||||||
def _figure_out_source(
|
def _figure_out_source(
|
||||||
record: logging.LogRecord,
|
record: logging.LogRecord,
|
||||||
paths_re: re.Pattern[str],
|
paths_re: re.Pattern[str],
|
||||||
extracted_tb: traceback.StackSummary | None = None,
|
extracted_tb: list[tuple[FrameType, int]] | None = None,
|
||||||
) -> tuple[str, int]:
|
) -> tuple[str, int]:
|
||||||
"""Figure out where a log message came from."""
|
"""Figure out where a log message came from."""
|
||||||
# If a stack trace exists, extract file names from the entire call stack.
|
# If a stack trace exists, extract file names from the entire call stack.
|
||||||
# The other case is when a regular "log" is made (without an attached
|
# The other case is when a regular "log" is made (without an attached
|
||||||
# exception). In that case, just use the file where the log was made from.
|
# exception). In that case, just use the file where the log was made from.
|
||||||
if record.exc_info:
|
if record.exc_info:
|
||||||
|
source: list[tuple[FrameType, int]] = extracted_tb or list(
|
||||||
|
traceback.walk_tb(record.exc_info[2])
|
||||||
|
)
|
||||||
stack = [
|
stack = [
|
||||||
(x[0], x[1])
|
(tb_frame.f_code.co_filename, tb_line_no) for tb_frame, tb_line_no in source
|
||||||
for x in (extracted_tb or traceback.extract_tb(record.exc_info[2]))
|
|
||||||
]
|
]
|
||||||
for i, (filename, _) in enumerate(stack):
|
for i, (filename, _) in enumerate(stack):
|
||||||
# Slice the stack to the first frame that matches
|
# Slice the stack to the first frame that matches
|
||||||
@ -176,6 +179,7 @@ class LogEntry:
|
|||||||
self,
|
self,
|
||||||
record: logging.LogRecord,
|
record: logging.LogRecord,
|
||||||
paths_re: re.Pattern,
|
paths_re: re.Pattern,
|
||||||
|
formatter: logging.Formatter | None = None,
|
||||||
figure_out_source: bool = False,
|
figure_out_source: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a log entry."""
|
"""Initialize a log entry."""
|
||||||
@ -186,14 +190,21 @@ class LogEntry:
|
|||||||
# This must be manually tested when changing the code.
|
# This must be manually tested when changing the code.
|
||||||
self.message = deque([_safe_get_message(record)], maxlen=5)
|
self.message = deque([_safe_get_message(record)], maxlen=5)
|
||||||
self.exception = ""
|
self.exception = ""
|
||||||
self.root_cause: str | None = None
|
self.root_cause: tuple[str, int, str] | None = None
|
||||||
extracted_tb: traceback.StackSummary | None = None
|
extracted_tb: list[tuple[FrameType, int]] | None = None
|
||||||
if record.exc_info:
|
if record.exc_info:
|
||||||
self.exception = "".join(traceback.format_exception(*record.exc_info))
|
if formatter and record.exc_text is None:
|
||||||
if extracted := traceback.extract_tb(record.exc_info[2]):
|
record.exc_text = formatter.formatException(record.exc_info)
|
||||||
|
self.exception = record.exc_text or ""
|
||||||
|
if extracted := list(traceback.walk_tb(record.exc_info[2])):
|
||||||
# Last line of traceback contains the root cause of the exception
|
# Last line of traceback contains the root cause of the exception
|
||||||
extracted_tb = extracted
|
extracted_tb = extracted
|
||||||
self.root_cause = str(extracted[-1])
|
tb_frame, tb_line_no = extracted[-1]
|
||||||
|
self.root_cause = (
|
||||||
|
tb_frame.f_code.co_filename,
|
||||||
|
tb_line_no,
|
||||||
|
tb_frame.f_code.co_name,
|
||||||
|
)
|
||||||
if figure_out_source:
|
if figure_out_source:
|
||||||
self.source = _figure_out_source(record, paths_re, extracted_tb)
|
self.source = _figure_out_source(record, paths_re, extracted_tb)
|
||||||
else:
|
else:
|
||||||
@ -273,7 +284,9 @@ class LogErrorHandler(logging.Handler):
|
|||||||
default upper limit is set to 50 (older entries are discarded) but can
|
default upper limit is set to 50 (older entries are discarded) but can
|
||||||
be changed if needed.
|
be changed if needed.
|
||||||
"""
|
"""
|
||||||
entry = LogEntry(record, self.paths_re, figure_out_source=True)
|
entry = LogEntry(
|
||||||
|
record, self.paths_re, formatter=self.formatter, figure_out_source=True
|
||||||
|
)
|
||||||
self.records.add_entry(entry)
|
self.records.add_entry(entry)
|
||||||
if self.fire_event:
|
if self.fire_event:
|
||||||
self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict())
|
self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict())
|
||||||
|
@ -870,7 +870,10 @@ class LogRelayHandler(logging.Handler):
|
|||||||
def emit(self, record: LogRecord) -> None:
|
def emit(self, record: LogRecord) -> None:
|
||||||
"""Relay log message via dispatcher."""
|
"""Relay log message via dispatcher."""
|
||||||
entry = LogEntry(
|
entry = LogEntry(
|
||||||
record, self.paths_re, figure_out_source=record.levelno >= logging.WARNING
|
record,
|
||||||
|
self.paths_re,
|
||||||
|
formatter=self.formatter,
|
||||||
|
figure_out_source=record.levelno >= logging.WARNING,
|
||||||
)
|
)
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self.hass,
|
self.hass,
|
||||||
|
@ -471,10 +471,35 @@ async def test__figure_out_source(hass: HomeAssistant) -> None:
|
|||||||
file, line_no = system_log._figure_out_source(
|
file, line_no = system_log._figure_out_source(
|
||||||
mock_record,
|
mock_record,
|
||||||
paths_re,
|
paths_re,
|
||||||
traceback.extract_tb(exc_info[2]),
|
list(traceback.walk_tb(exc_info[2])),
|
||||||
)
|
)
|
||||||
assert file == __file__
|
assert file == __file__
|
||||||
assert line_no != 5
|
assert line_no != 5
|
||||||
|
|
||||||
entry = system_log.LogEntry(mock_record, paths_re, figure_out_source=False)
|
entry = system_log.LogEntry(mock_record, paths_re, figure_out_source=False)
|
||||||
assert entry.source == ("figure_out_source is False", 5)
|
assert entry.source == ("figure_out_source is False", 5)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_formatting_exception(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that exceptions are formatted correctly."""
|
||||||
|
try:
|
||||||
|
raise ValueError("test")
|
||||||
|
except ValueError as ex:
|
||||||
|
exc_info = (type(ex), ex, ex.__traceback__)
|
||||||
|
mock_record = MagicMock(
|
||||||
|
pathname="figure_out_source is False",
|
||||||
|
lineno=5,
|
||||||
|
exc_info=exc_info,
|
||||||
|
exc_text=None,
|
||||||
|
)
|
||||||
|
regex_str = f"({__file__})"
|
||||||
|
paths_re = re.compile(regex_str)
|
||||||
|
|
||||||
|
mock_formatter = MagicMock(
|
||||||
|
formatException=MagicMock(return_value="formatted exception")
|
||||||
|
)
|
||||||
|
entry = system_log.LogEntry(
|
||||||
|
mock_record, paths_re, formatter=mock_formatter, figure_out_source=False
|
||||||
|
)
|
||||||
|
assert entry.exception == "formatted exception"
|
||||||
|
assert mock_record.exc_text == "formatted exception"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user