From 259a1542d8ec244ef99ab8319ca28ef83abd6e3d Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 25 May 2026 18:33:31 +0200 Subject: [PATCH] feat(phase-4): add write_audit_log() async helper (flush-not-commit, never-raises) - Creates backend/services/audit.py with write_audit_log() function - Uses session.flush() not session.commit() per D-14 architectural requirement - Catches and logs all exceptions (never re-raises) so audit failure is non-fatal - Correct AuditLog ORM attribute metadata_ (not metadata) per models.py --- backend/services/audit.py | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 backend/services/audit.py diff --git a/backend/services/audit.py b/backend/services/audit.py new file mode 100644 index 0000000..425319c --- /dev/null +++ b/backend/services/audit.py @@ -0,0 +1,58 @@ +""" +Audit log service helper for DocuVault — Phase 4. + +Provides write_audit_log(), a fire-and-forget helper called inline by API +handlers after successful operations (D-14). + +Key architectural constraints: + - Uses session.flush() NOT session.commit() (D-14: the caller owns the + transaction; the audit entry is flushed within the same transaction but + the commit remains the caller's responsibility). + - NEVER raises — audit failure must not abort the primary operation. + Exceptions are logged at WARNING level and silently swallowed. +""" +from __future__ import annotations + +import logging +import uuid +from typing import Optional + +from sqlalchemy.ext.asyncio import AsyncSession + +from db.models import AuditLog + +logger = logging.getLogger(__name__) + + +async def write_audit_log( + session: AsyncSession, + event_type: str, + user_id: Optional[uuid.UUID], + actor_id: Optional[uuid.UUID], + resource_id: Optional[uuid.UUID], + ip_address: Optional[str], + metadata_: Optional[dict] = None, +) -> None: + """Write an audit log entry within the caller's transaction. + + Never raises — audit failure is non-fatal. Exceptions are caught and + logged as warnings so the primary operation is never aborted. + + Uses session.flush() not session.commit() (D-14). The caller is responsible + for committing the transaction; this function merely ensures the audit row + is queued in the same unit of work. + """ + try: + entry = AuditLog( + event_type=event_type, + user_id=user_id, + actor_id=actor_id, + resource_id=resource_id, + ip_address=ip_address, + metadata_=metadata_, + ) + session.add(entry) + await session.flush() # flush within handler's existing transaction, not commit + except Exception as exc: + logger.warning("audit log write failed: %s", exc) + # Do not re-raise — audit failure must never abort the primary operation