fix(audit): threading.Lock on AuditLog.log + close (P1 bug)

detection thread and async heartbeat call log() concurrently.
Without a lock, two threads can both see today != _current_date
and double-open the file, corrupting the handle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-17 10:16:28 +00:00
parent c6714e8d5e
commit fd04fcd5e6

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import json import json
import threading
from datetime import datetime, date from datetime import datetime, date
from pathlib import Path from pathlib import Path
from typing import Callable, IO from typing import Callable, IO
@@ -16,21 +17,21 @@ class AuditLog:
self._clock: Callable[[], datetime] = clock or datetime.now self._clock: Callable[[], datetime] = clock or datetime.now
self._current_date: date | None = None self._current_date: date | None = None
self._fh: IO[str] | None = None self._fh: IO[str] | None = None
self._lock = threading.Lock()
def log(self, event: dict) -> None: def log(self, event: dict) -> None:
now = self._clock() now = self._clock()
today = now.date() today = now.date()
if today != self._current_date:
self._open(today)
if "ts" not in event: if "ts" not in event:
event = {**event, "ts": now.isoformat()} event = {**event, "ts": now.isoformat()}
with self._lock:
if today != self._current_date:
self._open(today)
assert self._fh is not None assert self._fh is not None
self._fh.write(json.dumps(event, separators=(",", ":")) + "\n") self._fh.write(json.dumps(event, separators=(",", ":")) + "\n")
def close(self) -> None: def close(self) -> None:
with self._lock:
if self._fh is not None: if self._fh is not None:
try: try:
self._fh.close() self._fh.close()