"""Audit log — תיעוד כל פעולה במערכת. כל פעולה מתועדת: מי, מתי, מה, על איזה תיק. """ from __future__ import annotations import json import logging from datetime import datetime from uuid import UUID, uuid4 from legal_mcp.services import db logger = logging.getLogger("audit") async def log_action( action: str, case_id: UUID | None = None, document_id: UUID | None = None, details: dict | None = None, user: str = "system", ) -> None: """רישום פעולה ב-audit log. Args: action: סוג הפעולה (upload, classify, extract_claims, set_outcome, write_block, export, etc.) case_id: מזהה תיק (אם רלוונטי) document_id: מזהה מסמך (אם רלוונטי) details: פרטים נוספים user: מזהה המשתמש """ pool = await db.get_pool() async with pool.acquire() as conn: await conn.execute( """INSERT INTO audit_log (id, action, case_id, document_id, details, actor, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7)""", uuid4(), action, case_id, document_id, json.dumps(details or {}, ensure_ascii=False, default=str), user, datetime.utcnow(), ) logger.info("AUDIT: %s | case=%s | user=%s | %s", action, case_id, user, json.dumps(details or {}, ensure_ascii=False)[:200]) async def log_action_safe( action: str, case_id: "UUID | None" = None, document_id: "UUID | None" = None, details: dict | None = None, user: str = "system", ) -> None: """Non-fatal audit: never let an audit-log failure break the caller's action. The authoritative integrity trail is git (X5 §2.1); audit_log is the 'who/what/when' observability layer, so a write failure is logged as a warning and swallowed. """ try: await log_action(action, case_id=case_id, document_id=document_id, details=details, user=user) except Exception as e: # noqa: BLE001 — observability must not break the op logger.warning("audit log_action failed (non-fatal) for %s: %s", action, e) async def get_audit_log( case_id: UUID | None = None, action: str | None = None, limit: int = 50, ) -> list[dict]: """שליפת audit log.""" pool = await db.get_pool() conditions = [] params: list = [] idx = 1 if case_id: conditions.append(f"case_id = ${idx}") params.append(case_id) idx += 1 if action: conditions.append(f"action = ${idx}") params.append(action) idx += 1 where = f"WHERE {' AND '.join(conditions)}" if conditions else "" params.append(limit) async with pool.acquire() as conn: rows = await conn.fetch( f"SELECT * FROM audit_log {where} ORDER BY created_at DESC LIMIT ${idx}", *params, ) return [dict(r) for r in rows]