Audit logging
SQLAdmin can record every create, update and delete made through the admin to
an audit backend. Configure one with the audit_backend argument of Admin.
The default is NullAuditBackend, which records nothing, so auditing is fully
opt-in.
Each change is described by a fixed AuditEntry dataclass:
@dataclass
class AuditEntry:
action: str # "create", "update" or "delete"
identity: str # the ModelView identity, e.g. "user"
pk: str | None # the affected object's identifier
changes: dict | None # submitted field values ("None" for deletes)
timestamp: datetime # UTC
Logging to your existing log stream
LoggingAuditBackend emits each entry through the standard logging module,
under the sqladmin.audit logger by default:
from sqladmin import Admin
from sqladmin.audit import LoggingAuditBackend
admin = Admin(app, engine, audit_backend=LoggingAuditBackend())
Persisting to your own table
DBAuditBackend writes entries to a database table — but SQLAdmin does not ship
an audit model, because the right shape depends on your app, most importantly
the type of your users' primary key (int, str or UUID) and the foreign key to
it. Instead you define the model and subclass the backend, overriding two
methods:
get_actor(request)— map the current request/session to your user's primary key (or any actor identifier).build_row(entry, actor, request)— turn anAuditEntryplus the resolved actor into an instance of your model.
The AuditEntry fields never change; only this mapping does.
from sqlalchemy import Column, Integer, String, JSON, DateTime, ForeignKey
from sqladmin import Admin
from sqladmin.audit import DBAuditBackend
class AuditLog(Base):
__tablename__ = "audit_log"
id = Column(Integer, primary_key=True)
action = Column(String(20))
table = Column(String(100))
object_pk = Column(String(100), nullable=True)
changes = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True))
# FK to your own user table; the PK type is entirely yours (int/str/UUID).
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
class MyAuditBackend(DBAuditBackend):
async def get_actor(self, request):
return request.session.get("user_id")
def build_row(self, entry, actor, request):
return AuditLog(
action=entry.action,
table=entry.identity,
object_pk=entry.pk,
changes=entry.changes,
created_at=entry.timestamp,
user_id=actor,
)
admin = Admin(app, engine, audit_backend=MyAuditBackend(session_maker))
Because the audit log is just one of your own models, you can register it as a
read-only ModelView to browse the trail inside the admin with the usual list,
filter and detail pages.
Note
Auditing is best-effort: a backend that raises is logged (to
sqladmin.audit) rather than propagated, so a misconfigured audit backend
cannot break an already-committed change. The DBAuditBackend writes in its
own transaction, separate from the change it records.
Writing a custom backend
For anything else — a message queue, an external audit service — subclass
AuditBackend directly and implement the single async log method:
from sqladmin.audit import AuditBackend
class QueueAuditBackend(AuditBackend):
async def log(self, entry, request):
await queue.publish(entry.__dict__)