Skip to main content
ImmutableLog logo
Back
DjangoPython

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.

bash
pip install requests

Copy 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.

python
# 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 headers

Never 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.

python
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.

python
# 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.

json
{
  "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`.

ConditionType (kind)Severity
Unhandled exceptionerrorhigh
4xx / 5xxerrorhigh
3xxinfolow
2xxsuccesslow

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.

python
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.

python
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.

python
# 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).

json
{
  "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\":\"{\\&quot;sku\\&quot;:\\&quot;INVALID\\&quot;}\"}}",
  "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:

StatusMeaning
202Event accepted and queued (new)
200Idempotency-Key already exists — duplicate: true
400Missing/empty Idempotency-Key or invalid_immutable_trail
403Inactive/expired subscription, scope or retention
413payload_too_large (server limit: 16KB)
429monthly_limit_exceeded — do not retry
503mempool_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 metaWhat it does in the SIEM
event_nameThe 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 loginauthentication).
client_ipBecomes source.ip with maximum precedence and provenance client_asserted. See the section below.
immutable_trailBecomes imtbl.immutable_trail — groups related events for investigation (e.g. one order, one user).
typeEvent severity: error / warning / info / success. Colors the badge in the listing. (event_type is accepted as a synonym.)
service, env, request_idMetadata: 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.ip with 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.
source.ip precedence:client_ipX-Forwarded-For (1º hop)X-Real-IPconnection IP

Provenance 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.

python
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

json
{
  "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.