Django Integration
Integrate ImmutableLog into any Django application with a middleware that automatically captures all HTTP requests — errors, successes, and warnings — without changing a single line of your business code.
Installation
The middleware requires no dependencies beyond Django and the `requests` library, which is likely already installed in your project. Copy the `middleware.py` file into your application's configuration module (e.g., `api_core/middleware.py`) and install the dependency if needed.
pip install requestsCopy the middleware.py file to your configuration module directory (e.g., api_core/middleware.py).
Configuration
Add the following variables to your `settings.py`. The `IMTBL_API_KEY` and `IMTBL_URL` variables are required — without them the middleware will not send any events. The remaining variables are optional and enrich the recorded metadata.
# settings.py
# Obrigatório / Required
IMTBL_API_KEY = "iml_live_xxxxxxxxxxxxxxxx" # sua chave de API / your API key
IMTBL_URL = "https://api.immutablelog.com" # URL base da API / API base URL
# Opcional / Optional
IMTBL_SERVICE_NAME = "meu-servico-django" # identifica o serviço / identifies the service
IMTBL_ENV = "production" # "production", "staging", "development"
IMTBL_HEADERS = {} # headers adicionais / extra headersNever expose IMTBL_API_KEY in client-side code or public repositories. Use server-side environment variables and load them via os.environ.get() or tools like django-environ.
import os
IMTBL_API_KEY = os.environ.get("IMTBL_API_KEY", "")
IMTBL_URL = os.environ.get("IMTBL_URL", "https://api.immutablelog.com")Register the Middleware
Add the middleware to your `MIDDLEWARE` list in `settings.py`. Order matters: place `ImmutableLogAuditMiddleware` right after Django's standard security middlewares to ensure all requests are captured, including those that raise unhandled exceptions.
# settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
# ImmutableLog — adicione após os middlewares padrão
# ImmutableLog — add after standard middlewares
"api_core.middleware.ImmutableLogAuditMiddleware",
]How it works
The middleware intercepts the full lifecycle of each HTTP request. In `process_request`, it records the start timestamp and generates a unique `request_id` (or reuses the `X-Request-Id` header if present). In `process_response`, it calculates latency and sends the event to ImmutableLog. In `process_exception`, it captures unhandled exceptions and sends an error event with exception details.
process_request
Records start timestamp and generates unique request_id
process_response
Calculates latency, classifies the event, and sends it to ImmutableLog
process_exception
Captures unhandled exceptions and sends error event with details
The X-Request-Id header is returned in all responses, enabling end-to-end request tracing across systems.
Payload Structure
Each event sent to ImmutableLog contains a rich, structured payload with full request context. The payload is serialized as a JSON string (field `payload`) and accompanied by classification metadata (field `meta`). The server computes the SHA-256 hash over the payload exactly as sent.
{
"payload": "{\"id\":\"d6b6c2e5-0c1a-4b92-9a62-2f2c4c2e9a2a\",\"kind\":\"success\",\"message\":\"GET /api/users/ concluído com sucesso\",\"timestamp\":\"2026-02-21T14:32:11.000Z\",\"context\":{\"ip\":\"192.168.1.100\",\"user_agent\":\"Mozilla/5.0\",\"user_id\":42,\"email\":\"user@example.com\"},\"request\":{\"request_id\":\"8d0b5f06-6d1f-4d3c-9b4f-9f5a2d7b3c1a\",\"method\":\"GET\",\"path\":\"/api/users/\",\"query_params\":null},\"metrics\":{\"latency_ms\":38,\"status_code\":200},\"severity\":\"low\",\"success\":{\"status_code\":200,\"result\":\"ok\"}}",
"meta": {
"type": "success",
"event_name": "http.GET.api:user-list",
"service": "meu-servico-django",
"request_id": "8d0b5f06-6d1f-4d3c-9b4f-9f5a2d7b3c1a",
"env": "production"
}
}Limit: Payload limit: 12KB per event. If the payload exceeds the limit, large fields (`error`, `request_body`) are automatically removed. As a last resort, only essential fields (`id`, `kind`, `message`, `timestamp`) are kept.
Event classification
The middleware automatically classifies each event based on the HTTP response status or the presence of an exception. Unhandled exceptions result in `error`. 2xx status results in `success`. 3xx results in `info`. 4xx and 5xx result in `error`. Redirects and informational responses result in `info`.
| Condition | Type (kind) | Severity |
|---|---|---|
| Unhandled exception | error | high |
| 4xx / 5xx | error | high |
| 3xx | info | low |
| 2xx | success | low |
Custom event names
By default, the middleware generates the event name from the HTTP method and Django's named route (e.g., `http.GET.api:user-me`). To override this name in a specific view, assign `request.imtbl_event_name` before returning the response. This enables granular traceability by business event.
from django.http import JsonResponse
def process_payment(request):
# Sobrescreve o nome do evento para este endpoint
# Override the event name for this endpoint
request.imtbl_event_name = "payment.processed"
# ... lógica de negócio / business logic ...
return JsonResponse({"status": "ok"})If not set, the middleware defaults to http.METHOD.view_name based on Django's URL resolver.
Immutable trail (immutable_trail)
A trail groups related events into a single auditable timeline. The middleware resolves the trail by priority: `request.imtbl_trail` set in the view → `X-Imtbl-Trail` header propagated by the client → `user-<username>` fallback from the authenticated user. The value is normalized by `sanitize_trail` before sending.
from django.http import JsonResponse
def run_flow(request, flow_id):
# Agrupa todos os eventos deste fluxo na mesma trilha
# Group all events of this flow under the same trail
request.imtbl_trail = f"flow-{flow_id}"
# ... lógica de negócio / business logic ...
return JsonResponse({"status": "ok"})The trail cannot be empty, exceed 256 characters, or contain `:` — violations return `400 invalid_immutable_trail`. `sanitize_trail` fixes the value (replaces `:` with `-` and truncates at 256) before sending.
Health check exclusion
The middleware automatically ignores events named `http.GET.health-check`, preventing health check calls from load balancers and orchestrators from polluting the ledger. Make sure your health check route is named `health-check` in the Django URL resolver.
# urls.py
from django.urls import path
from . import views
urlpatterns = [
# Nome "health-check" → ignorado automaticamente pelo middleware
# Name "health-check" → automatically ignored by the middleware
path("health/", views.health_check, name="health-check"),
path("api/users/", views.user_list, name="user-list"),
]Error enrichment
On error events (`status >= 400` or exception), the middleware includes additional details: SHA-256 hash of the request body (for audit without exposing data), full body if smaller than 2KB, exception class and message (if any), and a `retryable` flag indicating whether the error is transient (408, 429, 500, 502, 503, 504).
{
"payload": "{\"id\":\"a1b2c3d4-...\",\"kind\":\"error\",\"message\":\"POST /api/checkout/ falhou com exceção: ValueError\",\"context\":{\"ip\":\"10.0.0.1\",\"user_id\":7},\"request\":{\"method\":\"POST\",\"path\":\"/api/checkout/\"},\"metrics\":{\"latency_ms\":12,\"status_code\":500},\"severity\":\"high\",\"error\":{\"status_code\":500,\"retryable\":true,\"exception\":\"ValueError\",\"exception_message\":\"Invalid product SKU\",\"request_body_hash\":\"4f2b8e9a3c5d...\",\"request_body\":\"{\\"sku\\":\\"INVALID\\"}\"}}",
"meta": {
"type": "error",
"event_name": "http.POST.api:checkout",
"service": "meu-servico-django",
"env": "production"
}
}error.exception
Exception class name
error.exception_message
Exception message
error.request_body_hash
SHA-256 of body (when the request has a body)
error.retryable
True for: 408, 429, 500, 502, 503, 504
API status codes
The middleware is fire-and-forget and does not block your application's response, but it logs the ingestion API response (warning on 4xx/5xx). Codes returned by the POST /v1/events endpoint:
| Status | Meaning |
|---|---|
| 202 | Event accepted and queued (new) |
| 200 | Idempotency-Key already exists — duplicate: true |
| 400 | Missing/empty Idempotency-Key or invalid_immutable_trail |
| 403 | Inactive/expired subscription, scope or retention |
| 413 | payload_too_large (server limit: 16KB) |
| 429 | monthly_limit_exceeded — do not retry |
| 503 | mempool_full — transient, retryable |
meta fields
Beyond the payload, the meta object carries the fields the SIEM uses to normalize (ECS) and enrich the event. All meta values are strings.
| Field in meta | What it does in the SIEM |
|---|---|
event_name | The event label (e.g. http.GET.auth-me-user, Payment error) — the identifier shown in the listing. Becomes event.action and derives event.category (e.g. a name with login → authentication). |
client_ip | Becomes source.ip with maximum precedence and provenance client_asserted. See the section below. |
immutable_trail | Becomes imtbl.immutable_trail — groups related events for investigation (e.g. one order, one user). |
type | Event severity: error / warning / info / success. Colors the badge in the listing. (event_type is accepted as a synonym.) |
service, env, request_id | Metadata: indexed and queryable in the dashboard (search/filters). |
client_ip — end-user IP
The problem it solves
Between the browser and the core there are proxies/ALB. The IP the core sees on the connection is the previous hop (the client's backend or an AWS proxy), not the user's. Without action, source.ip would be the proxy IP — useless for geo, threat and IP-based detection. (Real example: XFF arrived as 44.192.13.3 (AWS) while the user was 179.110.4.205.)
The solution
The client's backend is the only one that sees the browser's real IP — so it forwards that IP in meta.client_ip when calling POST /v1/events.
How the SIEM handles it
- Validates it is an IP (v4/v6). A non-IP value is ignored — it does not drop the event.
- Writes to
source.ipwith maximum precedence (see order below). - Stamps
imtbl.source_ip_origin = "client_asserted"— the UI shows the “User IP” badge. - Feeds GeoIP, threat intel and IP-based detection rules.
client_ipX-Forwarded-For (1º hop)X-Real-IPconnection IPProvenance and trust
client_ip is asserted by the tenant — the client's backend claims the IP, and it is spoofable by whoever controls that backend. However it is self-contained to the tenant (it never crosses another tenant's boundary). A threat/geo hit on a client_asserted value is a claim, not an edge observation.
How to obtain the IP in your backend
Take the first IP from X-Forwarded-For (closest to the user); as a fallback, use the connection IP.
def get_client_ip(request):
xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
if xff:
return xff.split(",")[0].strip() # primeiro hop = IP do usuario
return request.META.get("REMOTE_ADDR")
# ao montar o evento:
meta = {"event_name": "user.login", "client_ip": get_client_ip(request)}Payload example
{
"payload": "{\"user\":\"bob\",\"action\":\"login\"}",
"meta": {
"event_name": "user.login",
"client_ip": "179.110.4.205",
"immutable_trail": "user-bob"
}
}PII note: client_ip is PII and gets sealed into the immutable block (permanent). Decide consciously between forensic evidence and “right to be forgotten” before sending it.
This documentation reflects the current integration behavior. For questions or advanced integrations, contact the support team.
