Audit Logging¶
Track all changes made through the admin panel.
Overview¶
Every create, update, and delete action is automatically recorded. No configuration needed — it works out of the box for all registered models.
What Gets Logged¶
| Action | What's Recorded |
|---|---|
| CREATE | Full object snapshot |
| UPDATE | Before/after diff + full snapshot |
| DELETE | Full object snapshot before deletion |
Audit Log Schema¶
CREATE TABLE admin_audit_log (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER REFERENCES admin_users(id),
user_email VARCHAR(255), -- Denormalized for display
action VARCHAR(10) NOT NULL, -- CREATE | UPDATE | DELETE
model_name VARCHAR(255) NOT NULL, -- e.g., "Product"
table_name VARCHAR(255) NOT NULL, -- e.g., "products"
object_id VARCHAR(255) NOT NULL, -- PK as string
object_repr VARCHAR(500), -- Human label
changes JSONB, -- Diff for UPDATE
full_snapshot JSONB, -- Full object state
ip_address VARCHAR(45), -- IPv4 or IPv6
user_agent TEXT,
timestamp TIMESTAMP WITH TIME ZONE DEFAULT now()
);
Change Diff Format¶
For UPDATE actions, only changed fields are recorded:
How It Works¶
The system uses SQLAlchemy event hooks:
# Attached automatically at startup
@event.listens_for(session_factory, "before_flush")
def before_flush(session, flush_context, instances):
# Capture state before changes
for obj in session.dirty:
obj._audit_before = snapshot(obj)
@event.listens_for(session_factory, "after_flush")
def after_flush(session, flush_context):
# Record changes after flush
for obj in session.new:
write_audit(session, user, "CREATE", obj)
for obj in session.dirty:
before = obj._audit_before
after = snapshot(obj)
diff = compute_diff(before, after)
if diff:
write_audit(session, user, "UPDATE", obj, diff)
for obj in session.deleted:
write_audit(session, user, "DELETE", obj)
Audit Log UI¶
Access the audit log at /admin/audit-log/:
Features¶
- Timeline-style feed sorted by timestamp
- Color coding: green = CREATE, yellow = UPDATE, red = DELETE
- Each entry shows: timestamp, user, action, model, object
- Click to expand full diff or snapshot
- Diff view: before/after table with highlighted changes
Filters¶
Filter the audit log by:
- Model:
?model=Product - User:
?user_id=5 - Action:
?action=UPDATE - Date range:
?from=2024-01-01&to=2024-12-31 - Object:
?object_id=42
Per-Object History¶
Every edit form has a "History" tab showing all audit entries for that specific record.
Configuration¶
Enable/Disable Audit Logging¶
Audit Retention¶
Set how long to keep audit logs:
admin = Admin(
app=app,
engine=engine,
secret_key="...",
audit_retention_days=365, # Keep for 1 year
)
Default: keep forever (no purge).
Purge Old Logs¶
Run manually or schedule as a cron job:
from fastapi_admin_kit.audit import purge_old_logs
# Delete logs older than retention period
purge_old_logs(session, retention_days=365)
Custom Audit Writer¶
Override the default audit writer:
from fastapi_admin_kit.audit import AuditWriter
class MyAuditWriter(AuditWriter):
def write(self, session, user, action, obj, diff=None, snapshot=None):
# Custom write logic
# e.g., send to external service
pass
admin = Admin(
app=app,
engine=engine,
secret_key="...",
audit_writer=MyAuditWriter(),
)
External Audit Sinks¶
Send audit logs to external services:
from fastapi_admin_kit.audit import AuditSink
class ElasticSearchSink(AuditSink):
def emit(self, entry):
# Send to Elasticsearch
es.index(index="audit-logs", body=entry.to_dict())
class WebhookSink(AuditSink):
def emit(self, entry):
# Send to webhook
requests.post("https://your-service.com/audit", json=entry.to_dict())
admin = Admin(
app=app,
engine=engine,
secret_key="...",
audit_sinks=[ElasticSearchSink(), WebhookSink()],
)
Context Variable¶
The current user is available via a context variable:
from fastapi_admin_kit.audit.context import get_audit_user
# In any code path
user = get_audit_user()
if user:
print(f"Action by: {user.email}")
This is set by middleware on every admin request.
Snapshot Function¶
Objects are serialized to JSON-safe dicts:
from fastapi_admin_kit.audit.diff import snapshot, serialize_value
# snapshot() converts a SQLAlchemy object to a dict
data = snapshot(product_instance)
# serialize_value() makes individual values JSON-safe
# - datetime → ISO format string
# - Decimal → float
# - UUID → string
# - bytes → "<binary>"
Performance Considerations¶
Indexes¶
The audit log table has these indexes for fast queries:
CREATE INDEX idx_audit_model ON admin_audit_log(model_name, table_name);
CREATE INDEX idx_audit_user ON admin_audit_log(user_id);
CREATE INDEX idx_audit_timestamp ON admin_audit_log(timestamp DESC);
CREATE INDEX idx_audit_object ON admin_audit_log(table_name, object_id);
Large Tables¶
For high-traffic tables, consider:
- Disable audit per model:
-
Use external sinks instead of database storage
-
Purge regularly with
audit_retention_days
Next Steps¶
- Plugins — Extend with custom plugins
- Configuration — More configuration options
- API Reference — Admin API documentation