-
Notifications
You must be signed in to change notification settings - Fork 747
Implement logging through Systemd native Journal protocol #6006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ | |
| import queue | ||
| from typing import Any | ||
|
|
||
| from logging_journald import Facility, JournaldLogHandler | ||
|
|
||
|
|
||
| class AddonLoggerAdapter(logging.LoggerAdapter): | ||
| """Logging Adapter which prepends log entries with add-on name.""" | ||
|
|
@@ -59,6 +61,52 @@ | |
| self.listener = None | ||
|
|
||
|
|
||
| class HAOSLogHandler(JournaldLogHandler): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the Core should log through Systemd as well, I think it would be good to extract this to a shared Python package. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah make sense, it is fine by me to have this in Supervisor for the time being. |
||
| """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") as f: | ||
| for line in f: | ||
| if "/docker/containers/" in line: | ||
| container_id = line.split("/docker/containers/")[-1] | ||
| return container_id.split("/")[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: | ||
| self._fallback(record) | ||
|
|
||
|
|
||
| def activate_log_queue_handler() -> None: | ||
| """Migrate the existing log handlers to use the queue. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,25 @@ | |
| from datetime import UTC, datetime | ||
| from functools import wraps | ||
| import json | ||
| import logging | ||
| from types import MappingProxyType | ||
|
|
||
| from aiohttp import ClientResponse | ||
| from colorlog.escape_codes import escape_codes, parse_colors | ||
|
|
||
| from supervisor.exceptions import MalformedBinaryEntryError | ||
| 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]): | ||
|
|
@@ -24,15 +38,34 @@ def decorator(func): | |
| def wrapper(*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 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It appears that nothing else is using this facility in HAOS currently so it's fairly reliable indicator it's "our" logs.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I see this is syslog backward compatibility, and the numbers are from RFC5424, interesting. It seems in my logs that quite some entries omit any I see the library currently maps the standard logging priorities to the systemd-journald priority levels, maybe it would be better to store the Python logging level (which also can have custom levels) as a separate field (e.,g. |
||
|
|
||
|
|
||
| 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"]) | ||
| def journal_plain_formatter(entries: dict[str, str]) -> str: | ||
| """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"] | ||
|
|
||
|
|
||
|
|
@@ -58,7 +91,11 @@ def journal_verbose_formatter(entries: dict[str, str]) -> str: | |
| 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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, this is probably side-effect of the IDE refactoring tool, shouldn't be here.