Fast, tamper-evident event auditing (hash chain) for Python apps, with a Rust core.
Project description
rust-py-audit
Event audit logging library for Python applications, with a Rust core.
Records audit events (who did what, when, on which resource) in a fast, structured way, and chains each event to the previous one with SHA-256 โ any later edit, deletion, or reordering of the log file is detectable with verify().
Features
AuditLoggerโ simple API:log(...),verify(),last_hash()- Hash chain (SHA-256) โ each event embeds the hash of the previous event; altering any recorded event breaks the chain in a detectable way
- JSONL storage โ one event per line, append-only, no database required
- Thread-safe โ a single
AuditLoggercan be shared across threads (e.g. a multi-threaded WSGI server, or one middleware instance serving concurrent requests); the hash chain stays linear under concurrency - Free-form
metadataโ any JSON-serializabledict(IP, reason, request_id, etc.) - FastAPI middleware โ automatically logs state-changing requests (POST/PUT/PATCH/DELETE)
- Django middleware โ same idea, supports WSGI and ASGI
- ImmutableLog integration โ
local/remote/hybridmodes, automatic retry, pending queue, andflush_pending()(see dedicated section) - Rust core โ hash generation, serialization, I/O, and the ImmutableLog HTTP client all run in Rust via PyO3; the Python API stays simple
Requirements
- Python 3.10+
- No required runtime dependencies
Optional, installed separately:
fastapi+starletteโ forrust_py_audit.fastapi.AuditMiddlewaredjangoโ forrust_py_audit.django.AuditMiddleware
Installation
pip install rust-py-audit
With optional extras:
pip install "rust-py-audit[fastapi]"
pip install "rust-py-audit[django]"
Quick Start
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", "reason": "duplicate invoice"},
)
print(event["id"]) # uuid v4
print(event["hash"]) # sha256, 64 hex characters
print(audit.last_hash()) # hash of the last recorded event
result = audit.verify()
print(result)
# {"valid": True, "total_events": 1, "last_hash": "..."}
Chain integrity
Each event records the hash of the previous event (previous_hash) and its own hash (hash), computed from the event's content + previous_hash. The first event in the chain has previous_hash = null.
{"id":"evt_123","timestamp":"2026-06-17T10:00:00Z","app_name":"billing-api","actor_id":"user_123","action":"DELETE_INVOICE","resource":"invoice","resource_id":"inv_987","metadata":{"ip":"192.168.0.10"},"previous_hash":null,"hash":"abc123..."}
verify() re-reads the file from scratch and recomputes everything โ it never trusts any in-memory cache:
result = audit.verify()
If the chain is intact:
{"valid": True, "total_events": 10, "last_hash": "..."}
If any event was edited, removed, or reordered:
{"valid": False, "total_events": 10, "error_index": 4, "reason": "hash_mismatch"}
# or "reason": "broken_chain" (removed/reordered/forged event)
FastAPI
from fastapi import FastAPI
from rust_py_audit.fastapi import AuditMiddleware
app = FastAPI()
app.add_middleware(AuditMiddleware, app_name="billing-api", file_path="./audit.jsonl")
@app.delete("/invoices/{invoice_id}")
async def delete_invoice(invoice_id: str):
return {"deleted": invoice_id}
By default, only POST/PUT/PATCH/DELETE requests are logged. actor_id comes from the X-User-Id header (adjustable via actor_header=); falls back to "anonymous" if absent.
See the full example in examples/fastapi_app.py.
Django
# settings.py
MIDDLEWARE = [
"rust_py_audit.django.AuditMiddleware",
# ... other middlewares ...
]
# Optional:
RUST_PY_AUDIT_APP_NAME = "my-django-app"
RUST_PY_AUDIT_FILE_PATH = "./audit.jsonl"
RUST_PY_AUDIT_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
actor_id comes from request.user.pk when there's an authenticated user (via django.contrib.auth); falls back to "anonymous" otherwise. The middleware supports both WSGI and ASGI applications automatically.
See the full example in examples/django_example/.
ImmutableLog Integration
rust_py_audit can send each audit event to ImmutableLog (documentation), in addition to โ or instead of โ writing locally.
Operating modes
mode |
Writes local JSONL | Sends to ImmutableLog | Typical use |
|---|---|---|---|
"local" (default) |
โ | โ | The library's original behavior, no external dependency |
"remote" |
โ (except for pending entries, see below) | โ | ImmutableLog is the single source of truth; a delivery failure raises an exception |
"hybrid" |
โ | โ | Local chain + remote receipt; a delivery failure NEVER raises โ it becomes delivery_status="pending" |
mode="local" is the default โ existing code calling AuditLogger(app_name, file_path) keeps working unchanged.
Basic example
from rust_py_audit import AuditLogger
audit = AuditLogger(
app_name="billing-api",
file_path="./audit.jsonl",
mode="hybrid",
immutablelog_url="https://api.immutablelog.com",
immutablelog_api_key="iml_live_xxxxx",
timeout_ms=500,
retry_enabled=True,
max_retries=3,
)
event = audit.log(
actor_id="user_123",
action="DELETE_INVOICE",
resource="invoice",
resource_id="inv_987",
metadata={"ip": "192.168.0.10", "reason": "duplicate invoice"},
)
print(event["immutablelog"])
# {"status": "delivered", "tx_id": "tx_...", "payload_hash": "...", ...}
# or {"status": "pending", "tx_id": None, ...} if delivery failed (hybrid mode)
# Retries delivery of every event still marked "pending":
print(audit.flush_pending())
# {"flushed": 1, "still_pending": 0, "total": 1}
Environment variables
mode, immutablelog_url, and immutablelog_api_key accept None (the default) to fall back to an environment variable โ handy for not hardcoding credentials:
export RUST_PY_AUDIT_MODE=hybrid
export IMMUTABLELOG_URL=https://api.immutablelog.com
export IMMUTABLELOG_API_KEY=iml_live_xxxxx
# Without passing mode/immutablelog_url/immutablelog_api_key explicitly,
# they come from the environment variables above:
audit = AuditLogger(app_name="billing-api", file_path="./audit.jsonl")
An explicit parameter always takes priority over the environment variable. mode="remote"/"hybrid" without immutablelog_url/immutablelog_api_key (neither as a parameter nor as an env var) raises ValueError when the AuditLogger is created โ failing fast instead of only on the first log() call.
Severity, immutable_trail, and env
audit.log(...) accepts two optional parameters that only affect what gets sent to ImmutableLog (they never enter the hash):
event = audit.log(
actor_id="user_123",
action="DELETE_INVOICE",
resource="invoice",
resource_id="inv_987",
severity="error", # meta.type โ defaults to "info" if omitted
immutable_trail="order-2026-00441", # meta.immutable_trail โ groups related events
)
severitymust be one of"error","warning","info","success"โ any other value raisesValueError, in anymode(even"local", whereseverityis just stored without being used).immutable_trailis sanitized automatically (trimmed,:replaced with-, truncated at 256 chars); if it ends up empty after that, the field is omitted instead of being sent broken.- Both are preserved locally (without affecting
hash) precisely so thatflush_pending()can resend later with the same original classification. immutablelog_env(on theAuditLoggerconstructor, falling back to theIMMUTABLELOG_ENVenv var) setsmeta.envโ useful for tellingstaging/productionapart in ImmutableLog.
FastAPI
from fastapi import FastAPI
from rust_py_audit.fastapi import AuditMiddleware
app = FastAPI()
app.add_middleware(
AuditMiddleware,
app_name="billing-api",
file_path="./audit.jsonl",
mode="hybrid",
immutablelog_url="https://api.immutablelog.com",
immutablelog_api_key="iml_live_xxxxx",
immutablelog_env="production",
trail_header="X-Audit-Trail", # default โ read from the request, becomes meta.immutable_trail
)
In mode="remote"/"hybrid", the middleware computes severity automatically from the response's status_code (>=400 โ "error", 300-399 โ "info", 200-299 โ "success"). In mode="remote", if delivery fails the middleware logs a logging.warning(...) and moves on โ an audit failure never takes down the actual response already computed by the application.
Django
# settings.py
MIDDLEWARE = [
"rust_py_audit.django.AuditMiddleware",
# ... other middlewares ...
]
RUST_PY_AUDIT_MODE = "hybrid"
RUST_PY_AUDIT_FILE_PATH = "./audit.jsonl"
RUST_PY_AUDIT_IMMUTABLELOG_URL = "https://api.immutablelog.com"
RUST_PY_AUDIT_IMMUTABLELOG_API_KEY = "iml_live_xxxxx"
RUST_PY_AUDIT_IMMUTABLELOG_ENV = "production"
RUST_PY_AUDIT_TRAIL_HEADER = "X-Audit-Trail" # default โ read from the request, becomes meta.immutable_trail
Same behavior as FastAPI: severity computed from status_code, and delivery failures logged via logging.warning(...) without affecting the response.
Retry and idempotency
retry_enabled/max_retriescontrol how many times a retryable failure is retried (the sameIdempotency-Keyis used on every attempt โ never creates duplicate events on ImmutableLog).- Retryable:
5xxand timeouts. - Permanent (never retried):
400,401,403,429, and any other client error. - In
mode="remote", exhausting retries (or a permanent error) raisesRuntimeError. - In
mode="hybrid", the same scenario marks the event asdelivery_status="pending", writes it toaudit.pending.jsonl, and never raises โ callaudit.flush_pending()(manually, or from a cron/worker) to retry later.
Integrity guarantee
The local hash (event["hash"]) is computed before any delivery attempt and never includes the immutablelog field โ the remote receipt is operational metadata, attached afterward, and never invalidates verify():
audit.log(...) # hash computed, event already recorded/chained
audit.flush_pending() # only updates event["immutablelog"]; event["hash"] doesn't change
audit.verify() # still valid, even after flush_pending()
API Reference
AuditLogger(app_name, file_path="./audit.jsonl", mode=None, immutablelog_url=None, immutablelog_api_key=None, timeout_ms=500, retry_enabled=True, max_retries=3, immutablelog_env=None)
| Parameter | Type | Description |
|---|---|---|
app_name |
str |
Application name, recorded on every event |
file_path |
str |
Path to the JSONL file. If it already exists, the chain resumes from the last recorded hash |
mode |
str | None |
"local" (default) / "remote" / "hybrid". None falls back to RUST_PY_AUDIT_MODE, and finally to "local" |
immutablelog_url |
str | None |
ImmutableLog base URL. None falls back to IMMUTABLELOG_URL. Required (one way or another) in mode="remote"/"hybrid" |
immutablelog_api_key |
str | None |
API key (Bearer). None falls back to IMMUTABLELOG_API_KEY. Same requirement as immutablelog_url |
timeout_ms |
int |
HTTP request timeout to ImmutableLog, in milliseconds |
retry_enabled |
bool |
If True, retries retryable errors (5xx, timeout) up to max_retries times |
max_retries |
int |
Maximum number of retries (in addition to the initial attempt) |
immutablelog_env |
str | None |
Logical environment (meta.env, e.g. "production"). None falls back to IMMUTABLELOG_ENV; if neither is set, the field is omitted |
See ImmutableLog Integration for details on each mode.
audit.log(actor_id, action, resource, resource_id, metadata=None, severity=None, immutable_trail=None) โ dict
Records an event and returns the full event (already with id, timestamp, hash, etc.) as a dict. severity/immutable_trail are optional and only affect delivery to ImmutableLog โ see Severity, immutable_trail, and env.
| Event field | Type | Description |
|---|---|---|
id |
str |
UUID v4 |
timestamp |
str |
RFC3339 / UTC, e.g.: 2026-06-17T10:00:00Z |
app_name |
str |
Comes from the AuditLogger |
actor_id |
str |
Who performed the action |
action |
str |
E.g.: DELETE_INVOICE |
resource |
str |
E.g.: invoice |
resource_id |
str |
E.g.: inv_987 |
metadata |
dict |
Free-form โ any JSON-serializable value |
previous_hash |
str | None |
Hash of the previous event in the chain |
hash |
str |
SHA-256 (64 hex chars) of the event + previous_hash |
severity |
str | absent |
Only present if passed to log(). Becomes meta.type on ImmutableLog |
immutable_trail |
str | absent |
Only present if passed to log() (and not empty after sanitization). Becomes meta.immutable_trail |
immutablelog |
dict | absent |
Only present in mode="remote"/"hybrid". status is "delivered" or "pending"; other fields (tx_id, payload_hash, duplicate, request_id, ...) come from the ImmutableLog response |
In mode="remote", a permanent failure or exhausted retries raise RuntimeError instead of returning the dict.
audit.verify() โ dict
Re-reads the file and revalidates the entire chain from scratch. See Chain integrity. Unaffected by the immutablelog field โ only the hashed fields matter (see Integrity guarantee).
audit.last_hash() โ str | None
Hash of the last recorded event (in-memory cache, O(1)) โ None if no event has been recorded yet.
audit.flush_pending() โ dict
Attempts to redeliver to ImmutableLog every event marked as pending (recorded in audit.pending.jsonl, derived from file_path). Only relevant in mode="hybrid" โ other modes never populate this queue.
{"flushed": 1, "still_pending": 0, "total": 1}
On success, updates event["immutablelog"] in audit.jsonl (without changing hash) and removes the event from the queue. Events that fail again stay in the queue for the next call.
Building from Source
Requires Rust and maturin.
git clone https://github.com/robertolima-dev/rust-py-audit
cd rust-py-audit
python3 -m venv .venv
source .venv/bin/activate
pip install maturin
# Development build (installs into the current Python environment)
maturin develop
# Release wheel
maturin build --release
Running tests
# Rust unit tests
cargo test --no-default-features
# Python integration tests
pip install -e ".[dev]"
pytest tests/
Architecture
Python API (rust_py_audit)
โโโ AuditLogger(...) โโโบ src/audit_logger.rs (PyO3 #[pyclass])
โ โโโ log() โโโบ src/event.rs (AuditEvent)
โ โ โโโบ src/hash.rs (deterministic SHA-256)
โ โ โโโบ src/storage.rs (append/update in JSONL)
โ โ โโโบ src/immutablelog_client.rs (POST /v1/events, via reqwest)
โ โ โโโบ src/retry.rs (retry with Idempotency-Key)
โ โโโ verify() โโโบ src/verifier.rs (revalidates the local chain)
โ โโโ flush_pending() โโโบ redelivers audit.pending.jsonl
โ โโโ last_hash() โโโบ in-memory cache
โ
โโโ fastapi.AuditMiddleware โโโบ audit.log() on every mutating request
โโโ django.AuditMiddleware โโโบ same idea, WSGI/ASGI
src/immutablelog_config.rs holds AuditMode/ImmutableLogConfig; src/immutablelog_receipt.rs defines the ImmutableLogReceipt attached to each event.
The core is compiled into a native extension (.so/.pyd) by maturin and PyO3. The Python layer is thin โ it just routes calls and provides the framework adapters.
License
MIT โ see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rust_py_audit-0.2.4.tar.gz.
File metadata
- Download URL: rust_py_audit-0.2.4.tar.gz
- Upload date:
- Size: 78.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b8bf088ad3e9e4585c00f5cf9be7e8287caccb1fe97cbe33d63a72b5e80d476
|
|
| MD5 |
c6d6d4f1f1b44501236742537ba58ee1
|
|
| BLAKE2b-256 |
ad254f3f22c5df56b36d531eafdfcd45cdb7e03c30b6b5fb040a15d3ef0054b4
|
File details
Details for the file rust_py_audit-0.2.4-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: rust_py_audit-0.2.4-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 1.7 MB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0b7cf004e9a362eba0df10935d7a56e9ffebb90af2cd361501c6aa4498b49ef
|
|
| MD5 |
21a37a4a394a7c1db2cdc699e9a93119
|
|
| BLAKE2b-256 |
fd990d6505e45cd22aa315e66693551697e15374087a2b82d4518958cf334431
|
File details
Details for the file rust_py_audit-0.2.4-cp310-abi3-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: rust_py_audit-0.2.4-cp310-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 2.3 MB
- Tags: CPython 3.10+, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ee4d62251050689290b3b36eafb5078d600b2f31cd164b3038413a654730087
|
|
| MD5 |
6526c1473214f1cae61f13ae7e70a8df
|
|
| BLAKE2b-256 |
58579025c0b73f002b8026bc121722a94aec610c9fffab4aaa698fd54dde1535
|
File details
Details for the file rust_py_audit-0.2.4-cp310-abi3-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: rust_py_audit-0.2.4-cp310-abi3-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 2.3 MB
- Tags: CPython 3.10+, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed95c2e82ca4c3028f6ddb749d7320eb0b19f5bb49906b9657dc81bd4e254049
|
|
| MD5 |
7b0590d04720b487c1ccdceed5bf0638
|
|
| BLAKE2b-256 |
c4927f10bb363066991585deb9867a2f34e1e266c33e1f07bf16c5e093084473
|
File details
Details for the file rust_py_audit-0.2.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: rust_py_audit-0.2.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.1 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91abcea5484b2f784ca5437072518c48fab9437700160f8c1c019df9ea9bfde4
|
|
| MD5 |
ff2bf7b8c90c5f14518557261048c953
|
|
| BLAKE2b-256 |
e3bbadd6dab216f8c8d0c252f81ef9ddaf80638370653ae6fa17eaacc720a9c5
|
File details
Details for the file rust_py_audit-0.2.4-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: rust_py_audit-0.2.4-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 2.1 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7eb00de22ab4ac98b16a369c2f90984c1374fa0cb3f8435f9d1842ab3ef7e2f6
|
|
| MD5 |
9a6bfaf492cf5a2f0b4d4756ce580c70
|
|
| BLAKE2b-256 |
ef4b9be422c809de3b9ed9c9e5a13c96a0523a589d2984b7b9353a42f6957f37
|
File details
Details for the file rust_py_audit-0.2.4-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: rust_py_audit-0.2.4-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.9 MB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bf117aaf0d8c25d6eb8a77dd70d768768a4837549de72c797c1adf483a9fc09
|
|
| MD5 |
e4dc41e8cc66c958b7af50030d2a74c7
|
|
| BLAKE2b-256 |
14e0f7221032d77c54dfef1e47f9318ac4633bf5faf34093cf51aaa631aaa426
|
File details
Details for the file rust_py_audit-0.2.4-cp310-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: rust_py_audit-0.2.4-cp310-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.9 MB
- Tags: CPython 3.10+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f5d2a595ee49784d81cbc626d44d8daa29cd010c26e50aac656980f245142576
|
|
| MD5 |
8dcb54d2d7e97d7850c06c474dece81f
|
|
| BLAKE2b-256 |
b91ff216d902f64d73012640ab0128d8ef58962b0a16a57cd7f148c97d9017f6
|