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
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user