Files
legal-ai/mcp-server/src/legal_mcp/services/audit.py

97 lines
2.9 KiB
Python

"""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]