Drop-in audit trail for FastAPI, Django, and any Python backend.
Every event is chained with SHA-256 โ tampering doesn't go unnoticed. Ship the same events to
ImmutableLog when you need an external, verifiable receipt.
pip install rust-py-audit
Rust handles the hashing and I/O. Python stays simple.
id, timestamp, actor_id, action, resource
and free-form metadata โ generated automatically on every log() call.
Each event embeds the SHA-256 hash of the one before it. Edit, delete, or reorder a line and
verify() catches it.
One event per line, append-only. No database, no migrations โ just a file you can tail -f.
Add AuditMiddleware in one line. Logs every state-changing request (POST/PUT/PATCH/DELETE)
automatically.
Works with both WSGI and ASGI Django apps. Drop it in MIDDLEWARE and it logs the rest.
mode="local" (default, no network), "remote", or "hybrid" โ
ship events to ImmutableLog with retry,
idempotency keys, and a flush_pending() queue for anything that didn't land.
Hashing, serialization, file I/O, and the ImmutableLog HTTP client run in Rust and compile to a native
.so extension. No Rust needed to use it.
Share one AuditLogger across threads, or run gunicorn with N workers on the same file: a
cross-process file lock keeps the hash chain linear, so verify() stays valid under load.
AuditLogger(..., fsync=True) returns from log() only once the event is on disk โ
it survives a power loss, not just a crashed process. Off by default to keep log() at
~30 ยตs.
One middleware line, or call the core API directly.
from fastapi import FastAPI
from rust_py_audit.fastapi import AuditMiddleware
app = FastAPI()
# Logs every POST/PUT/PATCH/DELETE automatically
app.add_middleware(AuditMiddleware, app_name="billing-api")
@app.delete("/invoices/{invoice_id}")
async def delete_invoice(invoice_id: str):
return {"deleted": invoice_id}
# settings.py
MIDDLEWARE = [
"rust_py_audit.django.AuditMiddleware",
# ... rest of your middleware
]
# optional configuration
RUST_PY_AUDIT_APP_NAME = "billing-django"
RUST_PY_AUDIT_FILE_PATH = "./audit.jsonl"
from rust_py_audit import AuditLogger
audit = AuditLogger(app_name="billing-api", file_path="./audit.jsonl")
event = audit.log(
actor_id="user_123",
action="DELETE_INVOICE",
resource="invoice",
resource_id="inv_987",
metadata={"ip": "192.168.0.10"},
)
print(event["hash"]) # 64-char SHA-256 hex digest
print(audit.last_hash()) # same hash, O(1) lookup
result = audit.verify()
print(result) # {"valid": True, "total_events": 1, ...}
from rust_py_audit import AuditLogger
# mode="hybrid": writes audit.jsonl locally AND ships to ImmutableLog
audit = AuditLogger(
app_name="billing-api",
file_path="./audit.jsonl",
mode="hybrid",
immutablelog_url="https://api.immutablelog.com",
immutablelog_api_key="iml_live_xxxxx",
)
event = audit.log(
actor_id="user_123",
action="DELETE_INVOICE",
resource="invoice",
resource_id="inv_987",
)
print(event["immutablelog"])
# {"status": "delivered", "tx_id": "tx_...", ...}
# or {"status": "pending", ...} on a transient failure (queued) โ never raises
# or {"status": "failed", ...} on a permanent failure (not queued)
# Retry anything still marked "pending" (e.g. on a cron):
audit.flush_pending()
# {"flushed": 1, "failed": 0, "still_pending": 0, "total": 1}
Every event links to the one before it. Break the chain and verify() tells you exactly where.
hash_mismatch edited eventbroken_chain deleted eventbroken_chain reordered eventsbroken_chain forged previous_hashSHA-256 content + previous_hashJSONL stable field orderverify() rereads & recomputes everything{
"valid": True,
"total_events": 10,
"last_hash": "a1b2c3..."
}
{
"valid": False,
"total_events": 10,
"error_index": 4,
"reason": "hash_mismatch"
}
Rust hashes and persists. Python integrates. You verify.
The native .so extension is compiled once at publish time via maturin +
PyO3.
Your users just pip install โ no Rust toolchain required. ImmutableLog delivery is fully
optional โ mode="local" (the default) never touches the network.
Install from PyPI. No Rust, no compilers, no configuration.