"""Audit logging -- metadata only, never secret material.""" from __future__ import annotations import json import threading from datetime import datetime, timezone from .config import audit_log_path _lock = threading.Lock() def _scrub(record: dict) -> dict: return {k: v for k, v in record.items() if v is not None} def audit(operation: str, *, ref=None, adapter=None, target=None, result="ok", error_code=None, detail=None): entry = _scrub({ "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), "op": operation, "ref": ref, "adapter": adapter, "target": target, "result": result, "error": error_code, "detail": detail, }) line = json.dumps(entry, sort_keys=True) # Belt-and-suspenders: audit lines must never carry secret material. for banned in ("password", "secret", "token", "key"): if isinstance(detail, str) and banned in detail.lower(): entry["detail"] = "REDACTED_DETAIL" line = json.dumps(_scrub(entry), sort_keys=True) break try: with _lock, open(audit_log_path(), "a") as fh: fh.write(line + "\n") except OSError: pass # audit must never break operations