mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-10 03:19:37 +00:00
* Use newer StrEnum and IntEnum over Enum * Fix validation issue and remove unnecessary .value calls --------- Co-authored-by: Pascal Vizeli <pvizeli@syshack.ch>
155 lines
5.4 KiB
Python
155 lines
5.4 KiB
Python
"""Logs control for host."""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from aiohttp import ClientError, ClientSession, ClientTimeout
|
|
from aiohttp.client_reqrep import ClientResponse
|
|
from aiohttp.connector import UnixConnector
|
|
from aiohttp.hdrs import ACCEPT, RANGE
|
|
|
|
from ..coresys import CoreSys, CoreSysAttributes
|
|
from ..exceptions import ConfigurationFileError, HostLogError, HostNotSupportedError
|
|
from ..utils.json import read_json_file
|
|
from .const import PARAM_BOOT_ID, PARAM_SYSLOG_IDENTIFIER, LogFormat
|
|
|
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
|
|
# pylint: disable=no-member
|
|
SYSLOG_IDENTIFIERS_JSON: Path = (
|
|
Path(__file__).parents[1].joinpath("data/syslog-identifiers.json")
|
|
)
|
|
# pylint: enable=no-member
|
|
|
|
SYSTEMD_JOURNAL_GATEWAYD_SOCKET: Path = Path("/run/systemd-journal-gatewayd.sock")
|
|
|
|
# From systemd catalog for message IDs (`journalctl --dump-catalog``)
|
|
# -- b07a249cd024414a82dd00cd181378ff
|
|
# Subject: System start-up is now complete
|
|
# Defined-By: systemd
|
|
BOOT_IDS_QUERY = {"MESSAGE_ID": "b07a249cd024414a82dd00cd181378ff"}
|
|
|
|
|
|
class LogsControl(CoreSysAttributes):
|
|
"""Handle systemd-journal logs."""
|
|
|
|
def __init__(self, coresys: CoreSys):
|
|
"""Initialize host power handling."""
|
|
self.coresys: CoreSys = coresys
|
|
self._profiles: set[str] = set()
|
|
self._boot_ids: list[str] = []
|
|
self._default_identifiers: list[str] = []
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return True if Unix socket to systemd-journal-gatwayd is available."""
|
|
return SYSTEMD_JOURNAL_GATEWAYD_SOCKET.is_socket()
|
|
|
|
@property
|
|
def boot_ids(self) -> list[str]:
|
|
"""Get boot IDs from oldest to newest."""
|
|
return self._boot_ids
|
|
|
|
@property
|
|
def default_identifiers(self) -> list[str]:
|
|
"""Get default syslog identifiers."""
|
|
return self._default_identifiers
|
|
|
|
async def load(self) -> None:
|
|
"""Load log control."""
|
|
try:
|
|
self._default_identifiers = read_json_file(SYSLOG_IDENTIFIERS_JSON)
|
|
except ConfigurationFileError:
|
|
_LOGGER.warning(
|
|
"Can't read syslog identifiers json file from %s",
|
|
SYSLOG_IDENTIFIERS_JSON,
|
|
)
|
|
|
|
async def get_boot_id(self, offset: int = 0) -> str:
|
|
"""
|
|
Get ID of a boot by offset.
|
|
|
|
Current boot is offset = 0, negative numbers go that many in the past.
|
|
Positive numbers count up from the oldest boot.
|
|
"""
|
|
boot_ids = await self.get_boot_ids()
|
|
offset -= 1
|
|
if offset >= len(boot_ids) or abs(offset) > len(boot_ids):
|
|
raise ValueError(f"Logs only contain {len(boot_ids)} boots")
|
|
|
|
return boot_ids[offset]
|
|
|
|
async def get_boot_ids(self) -> list[str]:
|
|
"""Get boot IDs from oldest to newest."""
|
|
if self._boot_ids:
|
|
# Doesn't change without a reboot, no reason to query again once cached
|
|
return self._boot_ids
|
|
|
|
try:
|
|
async with self.journald_logs(
|
|
params=BOOT_IDS_QUERY,
|
|
accept=LogFormat.JSON,
|
|
timeout=ClientTimeout(total=20),
|
|
) as resp:
|
|
text = await resp.text()
|
|
self._boot_ids = [
|
|
json.loads(entry)[PARAM_BOOT_ID]
|
|
for entry in text.split("\n")
|
|
if entry
|
|
]
|
|
return self._boot_ids
|
|
except (ClientError, TimeoutError) as err:
|
|
raise HostLogError(
|
|
"Could not get a list of boot IDs from systemd-journal-gatewayd",
|
|
_LOGGER.error,
|
|
) from err
|
|
|
|
async def get_identifiers(self) -> list[str]:
|
|
"""Get syslog identifiers."""
|
|
try:
|
|
async with self.journald_logs(
|
|
path=f"/fields/{PARAM_SYSLOG_IDENTIFIER}",
|
|
timeout=ClientTimeout(total=20),
|
|
) as resp:
|
|
return [i for i in (await resp.text()).split("\n") if i]
|
|
except (ClientError, TimeoutError) as err:
|
|
raise HostLogError(
|
|
"Could not get a list of syslog identifiers from systemd-journal-gatewayd",
|
|
_LOGGER.error,
|
|
) from err
|
|
|
|
@asynccontextmanager
|
|
async def journald_logs(
|
|
self,
|
|
path: str = "/entries",
|
|
params: dict[str, str | list[str]] | None = None,
|
|
range_header: str | None = None,
|
|
accept: LogFormat = LogFormat.TEXT,
|
|
timeout: ClientTimeout | None = None,
|
|
) -> ClientResponse:
|
|
"""Get logs from systemd-journal-gatewayd.
|
|
|
|
See https://www.freedesktop.org/software/systemd/man/systemd-journal-gatewayd.service.html for params and more info.
|
|
"""
|
|
if not self.available:
|
|
raise HostNotSupportedError(
|
|
"No systemd-journal-gatewayd Unix socket available", _LOGGER.error
|
|
)
|
|
|
|
async with ClientSession(
|
|
connector=UnixConnector(path="/run/systemd-journal-gatewayd.sock")
|
|
) as session:
|
|
headers = {ACCEPT: accept}
|
|
if range_header:
|
|
headers[RANGE] = range_header
|
|
async with session.get(
|
|
f"http://localhost{path}",
|
|
headers=headers,
|
|
params=params or {},
|
|
timeout=timeout,
|
|
) as client_response:
|
|
yield client_response
|