Source code for logeverything.integrations.django
"""
Django middleware for LogEverything.
Standard Django middleware that adds correlation IDs and request logging.
Usage — add to ``MIDDLEWARE`` in ``settings.py``::
MIDDLEWARE = [
...
"logeverything.integrations.django.LogEverythingDjangoMiddleware",
...
]
Optional settings (in ``settings.py``)::
LOGEVERYTHING_LOGGER_NAME = "logeverything.http"
LOGEVERYTHING_EXCLUDE_PATHS = {"/health", "/metrics"}
LOGEVERYTHING_REQUEST_ID_HEADER = "X-Request-ID"
"""
import logging
import time
from typing import Any, Callable, Set
from logeverything.correlation import clear_correlation, set_correlation_id, set_request_context
[docs]
class LogEverythingDjangoMiddleware:
"""
Django middleware for automatic request correlation and logging.
Follows the standard Django middleware protocol (``__init__`` / ``__call__`` /
``process_exception``).
"""
def __init__(self, get_response: Callable):
self.get_response = get_response
# Read optional Django settings with sensible defaults
try:
from django.conf import settings
self.logger_name: str = getattr(
settings, "LOGEVERYTHING_LOGGER_NAME", "logeverything.http"
)
self.exclude_paths: Set[str] = set(
getattr(settings, "LOGEVERYTHING_EXCLUDE_PATHS", ("/health", "/metrics"))
)
self.request_id_header: str = getattr(
settings, "LOGEVERYTHING_REQUEST_ID_HEADER", "X-Request-ID"
)
except Exception:
self.logger_name = "logeverything.http"
self.exclude_paths = {"/health", "/metrics"}
self.request_id_header = "X-Request-ID"
self.logger = logging.getLogger(self.logger_name)
# Django normalises headers to META keys: HTTP_X_REQUEST_ID
self._meta_key = "HTTP_" + self.request_id_header.upper().replace("-", "_")
def __call__(self, request: Any) -> Any:
path = request.path
if path in self.exclude_paths:
return self.get_response(request)
# --- Correlation ---
incoming_id = request.META.get(self._meta_key, "")
cid = set_correlation_id(incoming_id or None)
method = request.method
client_ip = self._get_client_ip(request)
set_request_context({"method": method, "path": path, "client_ip": client_ip})
self.logger.info(">>> %s %s (client=%s)", method, path, client_ip)
start = time.perf_counter()
try:
response = self.get_response(request)
except Exception:
duration_ms = (time.perf_counter() - start) * 1000
self.logger.exception(
"<<< %s %s 500 (%.1fms) [unhandled exception]", method, path, duration_ms
)
raise
finally:
clear_correlation()
duration_ms = (time.perf_counter() - start) * 1000
self.logger.info("<<< %s %s %s (%.1fms)", method, path, response.status_code, duration_ms)
# Inject correlation header
response[self.request_id_header] = cid
return response
[docs]
def process_exception(self, request: Any, exception: BaseException) -> None:
"""Log unhandled exceptions with correlation context."""
self.logger.exception(
"Unhandled exception in %s %s: %s",
request.method,
request.path,
exception,
)
return None # Let Django's default exception handling continue
@staticmethod
def _get_client_ip(request: Any) -> str:
"""Extract client IP, respecting X-Forwarded-For."""
xff = request.META.get("HTTP_X_FORWARDED_FOR")
if xff:
return xff.split(",")[0].strip() # type: ignore[no-any-return]
return request.META.get("REMOTE_ADDR", "unknown") # type: ignore[no-any-return]