Spaces:
Running
fix(audit): catch all write errors so audit failures can't crash requests
Browse filesAuditLogger.log() raised on any filesystem or serialization failure,
propagating up through _write_audit and the route handler into the
middleware's generic 500 handler. For stream endpoints, the audit call
happens at the end of the event generator so the client still sees the
answer before the stream aborts — but for non-stream /ask and for any
injection-blocked request (which writes audit before returning), the
failure surfaces as "Internal server error" with no indication that
the underlying orchestrator worked fine.
An audit logger that can crash the application is a misdesigned audit
logger. Wrap the write body in try/except Exception, log the failure
via structlog as audit_write_failed, and return normally. This is the
correct contract regardless of the specific filesystem issue — a
misconfigured path, a rotation bug, a serialization bug, or a disk-full
condition should all degrade gracefully to "audit missed, request
served," not take down the request path.
Verified: PermissionError on a root-only path is caught, logged as
audit_write_failed via structlog, and the caller returns normally.
Success path and hash_ip are unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@@ -51,20 +51,33 @@ class AuditLogger:
|
|
| 51 |
def log(self, record: dict) -> None:
|
| 52 |
"""Append a record to the audit log.
|
| 53 |
|
| 54 |
-
Adds a timestamp if not present. Thread-safe.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
"""
|
| 56 |
if "timestamp" not in record:
|
| 57 |
record["timestamp"] = datetime.now(timezone.utc).isoformat()
|
| 58 |
|
| 59 |
-
|
| 60 |
-
self.
|
|
|
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
def hash_ip(self, ip: str) -> str:
|
| 70 |
"""HMAC-SHA256 hash an IP address. Keyed and irreversible."""
|
|
|
|
| 51 |
def log(self, record: dict) -> None:
|
| 52 |
"""Append a record to the audit log.
|
| 53 |
|
| 54 |
+
Adds a timestamp if not present. Thread-safe. Best-effort: any
|
| 55 |
+
exception during the write is caught and logged via structlog,
|
| 56 |
+
never raised. Audit writes must not be able to crash the request
|
| 57 |
+
path — a misconfigured filesystem, a rotation bug, or a
|
| 58 |
+
serialization issue should degrade gracefully to "audit missed,
|
| 59 |
+
request served," not "audit failed, 500 to user."
|
| 60 |
"""
|
| 61 |
if "timestamp" not in record:
|
| 62 |
record["timestamp"] = datetime.now(timezone.utc).isoformat()
|
| 63 |
|
| 64 |
+
try:
|
| 65 |
+
with self._lock:
|
| 66 |
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 67 |
|
| 68 |
+
if self.rotate and self.path.exists():
|
| 69 |
+
if self.path.stat().st_size >= self.max_size_bytes:
|
| 70 |
+
self._do_rotate()
|
| 71 |
|
| 72 |
+
with open(self.path, "a") as f:
|
| 73 |
+
f.write(json.dumps(record, default=str) + "\n")
|
| 74 |
+
except Exception as exc:
|
| 75 |
+
logger.error(
|
| 76 |
+
"audit_write_failed",
|
| 77 |
+
error=str(exc),
|
| 78 |
+
error_type=type(exc).__name__,
|
| 79 |
+
path=str(self.path),
|
| 80 |
+
)
|
| 81 |
|
| 82 |
def hash_ip(self, ip: str) -> str:
|
| 83 |
"""HMAC-SHA256 hash an IP address. Keyed and irreversible."""
|