mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-15 05:50:16 +00:00
Compare commits
2 Commits
copilot/su
...
systemd-jo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e440429e48 | ||
|
|
1ff0ee5256 |
@@ -17,6 +17,7 @@ faust-cchardet==2.1.19
|
|||||||
gitpython==3.1.44
|
gitpython==3.1.44
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
log-rate-limit==1.4.2
|
log-rate-limit==1.4.2
|
||||||
|
logging-journald==0.6.11
|
||||||
orjson==3.10.18
|
orjson==3.10.18
|
||||||
pulsectl==24.12.0
|
pulsectl==24.12.0
|
||||||
pyudev==0.24.3
|
pyudev==0.24.3
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from .services import ServiceManager
|
|||||||
from .store import StoreManager
|
from .store import StoreManager
|
||||||
from .supervisor import Supervisor
|
from .supervisor import Supervisor
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
|
from .utils.logging import HAOSLogHandler
|
||||||
from .utils.sentry import capture_exception, init_sentry
|
from .utils.sentry import capture_exception, init_sentry
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -246,7 +247,17 @@ def initialize_logging() -> None:
|
|||||||
# suppress overly verbose logs from libraries that aren't helpful
|
# suppress overly verbose logs from libraries that aren't helpful
|
||||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||||
|
|
||||||
logging.getLogger().handlers[0].setFormatter(
|
default_handler = logging.getLogger().handlers[0]
|
||||||
|
|
||||||
|
if HAOSLogHandler.is_available():
|
||||||
|
# replace the default logging handler with JournaldLogHandler
|
||||||
|
logging.getLogger().removeHandler(default_handler)
|
||||||
|
supervisor_name = os.environ[ENV_SUPERVISOR_NAME]
|
||||||
|
journald_handler = HAOSLogHandler(identifier=supervisor_name)
|
||||||
|
journald_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||||
|
logging.getLogger().addHandler(journald_handler)
|
||||||
|
else:
|
||||||
|
default_handler.setFormatter(
|
||||||
ColoredFormatter(
|
ColoredFormatter(
|
||||||
colorfmt,
|
colorfmt,
|
||||||
datefmt=datefmt,
|
datefmt=datefmt,
|
||||||
@@ -260,6 +271,7 @@ def initialize_logging() -> None:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
warnings.showwarning = warning_handler
|
warnings.showwarning = warning_handler
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ class SoundControl(CoreSysAttributes):
|
|||||||
profile.name == card.profile_active.name,
|
profile.name == card.profile_active.name,
|
||||||
)
|
)
|
||||||
for profile in card.profile_list
|
for profile in card.profile_list
|
||||||
if profile.available
|
if profile.is_available
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
for card in pulse.card_list()
|
for card in pulse.card_list()
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import logging.handlers
|
|||||||
import queue
|
import queue
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from logging_journald import Facility, JournaldLogHandler
|
||||||
|
|
||||||
|
|
||||||
class AddonLoggerAdapter(logging.LoggerAdapter):
|
class AddonLoggerAdapter(logging.LoggerAdapter):
|
||||||
"""Logging Adapter which prepends log entries with add-on name."""
|
"""Logging Adapter which prepends log entries with add-on name."""
|
||||||
@@ -59,6 +61,52 @@ class SupervisorQueueHandler(logging.handlers.QueueHandler):
|
|||||||
self.listener = None
|
self.listener = None
|
||||||
|
|
||||||
|
|
||||||
|
class HAOSLogHandler(JournaldLogHandler):
|
||||||
|
"""Log handler for writing logs to the Home Assistant OS Systemd Journal."""
|
||||||
|
|
||||||
|
SYSLOG_FACILITY = Facility.LOCAL7
|
||||||
|
|
||||||
|
def __init__(self, identifier: str | None = None) -> None:
|
||||||
|
"""Initialize the HAOS log handler."""
|
||||||
|
super().__init__(identifier=identifier, facility=HAOSLogHandler.SYSLOG_FACILITY)
|
||||||
|
self._container_id = self._get_container_id()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_container_id() -> str | None:
|
||||||
|
"""Get the container ID if running inside a Docker container."""
|
||||||
|
# Currently we only have this hacky way of getting the container ID,
|
||||||
|
# we (probably) cannot get it without having some cgroup namespaces
|
||||||
|
# mounted in the container or passed it there using other means.
|
||||||
|
# Not obtaining it will only result in the logs not being available
|
||||||
|
# through `docker logs` command, so it is not a critical issue.
|
||||||
|
with open("/proc/self/mountinfo", mode="rb") as f:
|
||||||
|
for line in f:
|
||||||
|
if b"/docker/containers/" in line:
|
||||||
|
container_id = line.split(b"/docker/containers/")[-1]
|
||||||
|
return str(container_id.split(b"/")[0])
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_available(cls) -> bool:
|
||||||
|
"""Check if the HAOS log handler can be used."""
|
||||||
|
return cls.SOCKET_PATH.exists()
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
"""Emit formatted log record to the Systemd Journal.
|
||||||
|
|
||||||
|
If CONTAINER_ID is known, add it to the fields to make the log record
|
||||||
|
available through `docker logs` command.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
formatted = self._format_record(record)
|
||||||
|
if self._container_id:
|
||||||
|
# only container ID is needed for interpretation through `docker logs`
|
||||||
|
formatted.append(("CONTAINER_ID", self._container_id))
|
||||||
|
self.transport.send(formatted)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
self._fallback(record)
|
||||||
|
|
||||||
|
|
||||||
def activate_log_queue_handler() -> None:
|
def activate_log_queue_handler() -> None:
|
||||||
"""Migrate the existing log handlers to use the queue.
|
"""Migrate the existing log handlers to use the queue.
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,25 @@ from collections.abc import AsyncGenerator
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
from types import MappingProxyType
|
||||||
|
|
||||||
from aiohttp import ClientResponse
|
from aiohttp import ClientResponse
|
||||||
|
from colorlog.escape_codes import escape_codes, parse_colors
|
||||||
|
|
||||||
from supervisor.exceptions import MalformedBinaryEntryError
|
from supervisor.exceptions import MalformedBinaryEntryError
|
||||||
from supervisor.host.const import LogFormatter
|
from supervisor.host.const import LogFormatter
|
||||||
|
from supervisor.utils.logging import HAOSLogHandler
|
||||||
|
|
||||||
|
_LOG_COLORS = MappingProxyType(
|
||||||
|
{
|
||||||
|
HAOSLogHandler.LEVELS[logging.DEBUG]: "cyan",
|
||||||
|
HAOSLogHandler.LEVELS[logging.INFO]: "green",
|
||||||
|
HAOSLogHandler.LEVELS[logging.WARNING]: "yellow",
|
||||||
|
HAOSLogHandler.LEVELS[logging.ERROR]: "red",
|
||||||
|
HAOSLogHandler.LEVELS[logging.CRITICAL]: "red",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def formatter(required_fields: list[str]):
|
def formatter(required_fields: list[str]):
|
||||||
@@ -24,15 +38,34 @@ def formatter(required_fields: list[str]):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
wrapper.required_fields = ["__CURSOR"] + required_fields
|
implicit_fields = ["__CURSOR", "SYSLOG_FACILITY", "PRIORITY"]
|
||||||
|
wrapper.required_fields = implicit_fields + required_fields
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_is_haos_log(entry: dict[str, str]) -> bool:
|
||||||
|
"""Check if the entry is a Home Assistant Operating System log entry."""
|
||||||
|
return entry.get("SYSLOG_FACILITY") == str(HAOSLogHandler.SYSLOG_FACILITY)
|
||||||
|
|
||||||
|
|
||||||
|
def colorize_message(message: str, priority: str | None = None) -> str:
|
||||||
|
"""Colorize a log message using ANSI escape codes based on its priority."""
|
||||||
|
if priority and priority.isdigit():
|
||||||
|
color = _LOG_COLORS.get(int(priority))
|
||||||
|
if color is not None:
|
||||||
|
escape_code = parse_colors(color)
|
||||||
|
return f"{escape_code}{message}{escape_codes['reset']}"
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
@formatter(["MESSAGE"])
|
@formatter(["MESSAGE"])
|
||||||
def journal_plain_formatter(entries: dict[str, str]) -> str:
|
def journal_plain_formatter(entries: dict[str, str]) -> str:
|
||||||
"""Format parsed journal entries as a plain message."""
|
"""Format parsed journal entries as a plain message."""
|
||||||
|
if _entry_is_haos_log(entries):
|
||||||
|
return colorize_message(entries["MESSAGE"], entries.get("PRIORITY"))
|
||||||
|
|
||||||
return entries["MESSAGE"]
|
return entries["MESSAGE"]
|
||||||
|
|
||||||
|
|
||||||
@@ -58,7 +91,11 @@ def journal_verbose_formatter(entries: dict[str, str]) -> str:
|
|||||||
else entries.get("SYSLOG_IDENTIFIER", "_UNKNOWN_")
|
else entries.get("SYSLOG_IDENTIFIER", "_UNKNOWN_")
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"{ts} {entries.get('_HOSTNAME', '')} {identifier}: {entries.get('MESSAGE', '')}"
|
message = entries.get("MESSAGE", "")
|
||||||
|
if message and _entry_is_haos_log(entries):
|
||||||
|
message = colorize_message(message, entries.get("PRIORITY"))
|
||||||
|
|
||||||
|
return f"{ts} {entries.get('_HOSTNAME', '')} {identifier}: {message}"
|
||||||
|
|
||||||
|
|
||||||
async def journal_logs_reader(
|
async def journal_logs_reader(
|
||||||
|
|||||||
Reference in New Issue
Block a user