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.
This commit is contained in:
teknium1
2026-09-14 18:49:46 -07:00
committed by Teknium
parent da1fb702c3
commit b847c1ea5e
2 changed files with 11 additions and 4 deletions
+10 -3
View File
@@ -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):
+1 -1
View File
@@ -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