From b847c1ea5e72317bfbc55448066946b1bcb1b56e Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:49:46 -0700 Subject: [PATCH] fix(logging): name an unavailable log file once instead of silently dropping records Builds on the salvaged EIO suppression: the reporter asked that logging "degrade gracefully and identify the affected path". Print one stderr line naming the file and errno when the stream first fails, reset the flag when `_open()` succeeds again so a later failure is reported anew. The contributor's test is replaced by one invariant: five records through a stream raising EIO produce zero tracebacks and exactly one path mention, and the next emit recovers into the real file. --- hermes_logging.py | 13 ++++++++++--- tests/test_hermes_logging.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/hermes_logging.py b/hermes_logging.py index b1f2ffa8a3..5461256c1f 100644 --- a/hermes_logging.py +++ b/hermes_logging.py @@ -267,6 +267,7 @@ class _ManagedRotatingFileHandler(RotatingFileHandler): def __init__(self, *args, **kwargs): from hermes_cli.config import is_managed self._managed = is_managed() + self._unavailable_reported = False super().__init__(*args, **kwargs) self._record_stream_stat() @@ -336,9 +337,14 @@ class _ManagedRotatingFileHandler(RotatingFileHandler): if _is_windows_concurrent_log_lock_timeout(exc): return if _is_unavailable_log_stream(exc): - # The QueueListener must not turn a transient filesystem failure - # into a traceback for every queued record. Drop the stale stream; - # the next emit will reopen it if the destination has recovered. + # The QueueListener must not turn a failing log destination into a traceback for + # every queued record. Name the path once, drop the stale stream; the next emit + # reopens it if the destination has recovered. + if not self._unavailable_reported: + self._unavailable_reported = True + _quietly(lambda: print( + f"hermes_logging: {self.baseFilename} unavailable ({exc}); " + "file logging paused until it recovers", file=_safe_stderr())) if self.stream is not None: _quietly(self.stream.close) self.stream = None # type: ignore[assignment] @@ -348,6 +354,7 @@ class _ManagedRotatingFileHandler(RotatingFileHandler): def _open(self): stream = super()._open() self._chmod_if_managed() + self._unavailable_reported = False # recovered: report again if it breaks anew return stream def doRollover(self): diff --git a/tests/test_hermes_logging.py b/tests/test_hermes_logging.py index 6a2073ea8b..0b2f6cdc6f 100644 --- a/tests/test_hermes_logging.py +++ b/tests/test_hermes_logging.py @@ -613,7 +613,7 @@ def test_eio_from_file_handler_names_the_path_once_then_recovers(tmp_path, capsy def writable(self): return True - def write(self, _s): + def write(self, *_a): raise OSError(5, "Input/output error") seek = tell = flush = write