Why I built django-system-audit
Signal-based audit logging is architecturally broken for compliance systems. Here is what I built instead.
Most Django audit libraries rely heavily on signals.
Signals are great for detecting that something happened. But they struggle to answer a more important question: why did it happen?
While working on compliance-heavy systems — regulated platforms with strict audit requirements, fintech systems with SOC2 obligations — this caused real problems:
- Duplicate audit logs from multiple signals firing on the same event
- Confusing actor attribution — was this the API, the Admin panel, or a background job?
- Missing request context like IP address or endpoint
- Audit trails that were hard to trust when debugging incidents
The question that kept coming up during compliance reviews: “Who did this, from where, and why?” Signals couldn’t answer it.
The core architectural problem
Signals capture effects. When you save a model, a signal fires. But the signal doesn’t know what triggered the save — it just knows the save happened.
This means:
# A signal-based audit logger sees this:
# "Order #1234 was updated"
# What compliance actually needs:
# "Order #1234 was updated by user@example.com
# via PUT /api/orders/1234/
# from IP 192.168.1.1
# because the payment status changed"
The difference isn’t cosmetic. In a compliance audit, “the record was updated” is useless. “The record was updated by this actor, through this interface, with this context” is what makes an audit trail legally defensible.
What I built instead
django-system-audit takes a different approach: explicit, intent-aware logging.
Instead of relying on signals, views and admin actions record audit events directly through a central service:
from system_audit import audit
class OrderViewSet(viewsets.ModelViewSet):
def update(self, request, *args, **kwargs):
response = super().update(request, *args, **kwargs)
audit.log(
actor=request.user,
action="order.updated",
target=self.get_object(),
context={"ip": request.META.get("REMOTE_ADDR")},
)
return response
Signals still exist as a fallback — but they are no longer the primary mechanism. Views capture intent. Signals catch what slips through.
The result
Cleaner audit trails. Correct actor attribution. Full request context. And when a compliance team asks “who changed this record and why” — you have an actual answer.
The library is on PyPI:
pip install django-system-audit
GitHub: django-system-audit
If you are building in fintech, SaaS, or any compliance-sensitive domain — I would love to hear how you handle audit logging and whether this approach resonates.
You might also like
I spent a day turning a frustration into a shipped VS Code extension
How Session Bridge AI grew from a simple context-saving tool into something with a full token dashboard — and what I learned building it in one long session.
I built a VS Code extension to solve my biggest frustration with AI coding tools
Every time I switched AI tools mid-session, I lost all context. So I built Session Bridge AI — a VS Code extension that maintains a live SESSION.md your next AI tool can pick up instantly.
From Zero to Production: Everything We Shipped in Session Bridge AI
Every time I switched AI tools mid-session, I lost all context. So I built Session Bridge AI — a VS Code extension that maintains a live SESSION.md your next AI tool can pick up instantly.