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 a single AuditLogger across threads โ multi-threaded WSGI workers or one middleware
instance serving concurrent requests. The hash chain stays linear under load.
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.