Coverage for packages / core / storage / audit.py: 38%
37 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-08 08:37 +1200
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-08 08:37 +1200
1"""Audit logging for data changes and corrections.
3Provides the AuditLogger class for recording and querying data modifications
4across the system, enabling traceability of data corrections.
5"""
7from __future__ import annotations
9from typing import Any
11from sqlalchemy import desc
12from sqlalchemy.orm import Session
14from packages.core.common.logging import get_logger
15from packages.core.storage.models import AuditLog
17logger = get_logger(__name__)
20class AuditLogger:
21 """Logger for recording and querying data changes.
23 Usage::
25 audit = AuditLogger()
26 audit.log_change(
27 session, table_name="starters", record_id="42",
28 action="CORRECT", old_values={"placing": 3}, new_values={"placing": 2},
29 changed_by="admin", change_reason="Manual correction from video review",
30 )
32 All methods are safe to call — failures are logged but never raised,
33 ensuring audit logging never blocks the primary operation.
34 """
36 @staticmethod
37 def log_change(
38 session: Session,
39 table_name: str,
40 record_id: str,
41 action: str,
42 old_values: dict[str, Any] | None = None,
43 new_values: dict[str, Any] | None = None,
44 changed_by: str | None = None,
45 change_reason: str | None = None,
46 ) -> AuditLog | None:
47 """Record a data change in the audit log.
49 Args:
50 session: Database session.
51 table_name: Name of the table that was changed.
52 record_id: Primary key value of the changed record (stringified).
53 action: Type of change — ``INSERT``, ``UPDATE``, ``DELETE``, or ``CORRECT``.
54 old_values: Snapshot of values before the change (optional).
55 new_values: Snapshot of values after the change (optional).
56 changed_by: Identifier of the user/system that made the change (optional).
57 change_reason: Human-readable reason for the change (optional).
59 Returns:
60 The created AuditLog entry, or ``None`` if logging failed.
61 """
62 action_upper = action.upper().strip()
63 if action_upper not in ("INSERT", "UPDATE", "DELETE", "CORRECT"):
64 logger.warning(
65 "Invalid audit action '%s' — must be INSERT, UPDATE, DELETE, or CORRECT",
66 action,
67 )
68 return None
70 try:
71 entry = AuditLog(
72 table_name=table_name,
73 record_id=str(record_id),
74 action=action_upper,
75 old_values=old_values,
76 new_values=new_values,
77 changed_by=changed_by,
78 change_reason=change_reason,
79 )
80 session.add(entry)
81 session.flush()
82 logger.debug(
83 "Audit log entry created",
84 extra={
85 "table_name": table_name,
86 "record_id": record_id,
87 "action": action_upper,
88 },
89 )
90 return entry
91 except Exception:
92 logger.exception(
93 "Failed to create audit log entry",
94 extra={
95 "table_name": table_name,
96 "record_id": record_id,
97 "action": action_upper,
98 },
99 )
100 return None
102 @staticmethod
103 def get_changes_for_record(
104 session: Session,
105 table_name: str,
106 record_id: str,
107 ) -> list[AuditLog]:
108 """Retrieve all audit log entries for a specific record.
110 Args:
111 session: Database session.
112 table_name: Name of the table.
113 record_id: Primary key value of the record (stringified).
115 Returns:
116 List of AuditLog entries, newest first.
117 """
118 try:
119 return (
120 session.query(AuditLog)
121 .filter(
122 AuditLog.table_name == table_name,
123 AuditLog.record_id == str(record_id),
124 )
125 .order_by(desc(AuditLog.created_at))
126 .all()
127 )
128 except Exception:
129 logger.exception(
130 "Failed to query audit log for record",
131 extra={"table_name": table_name, "record_id": record_id},
132 )
133 return []
135 @staticmethod
136 def get_recent_changes(
137 session: Session,
138 limit: int = 100,
139 ) -> list[AuditLog]:
140 """Retrieve the most recent audit log entries across all tables.
142 Args:
143 session: Database session.
144 limit: Maximum number of entries to return (default 100).
146 Returns:
147 List of recent AuditLog entries, newest first.
148 """
149 try:
150 return (
151 session.query(AuditLog)
152 .order_by(desc(AuditLog.created_at))
153 .limit(limit)
154 .all()
155 )
156 except Exception:
157 logger.exception("Failed to query recent audit log entries")
158 return []