""" Core middleware components. Provides Request ID tracking and other cross-cutting concerns. """ import logging import threading import uuid from django.utils.deprecation import MiddlewareMixin logger = logging.getLogger(__name__) # Thread-local storage for request context _request_context = threading.local() def get_request_id() -> str | None: """Get current request ID from thread-local storage.""" return getattr(_request_context, "request_id", None) def get_current_request(): """Get current request from thread-local storage.""" return getattr(_request_context, "request", None) class RequestIDMiddleware(MiddlewareMixin): """ Middleware that generates or extracts a unique request ID for each request. The request ID is: 1. Extracted from X-Request-ID header if present 2. Generated as UUID4 if not present 3. Added to response headers 4. Available via get_request_id() for logging Usage in logging: from apps.core.middleware import get_request_id logger.info(f"[{get_request_id()}] Processing request") """ REQUEST_ID_HEADER = "X-Request-ID" def process_request(self, request): """Extract or generate request ID and store in thread-local.""" request_id = request.headers.get(self.REQUEST_ID_HEADER) if not request_id: request_id = str(uuid.uuid4()) request.request_id = request_id _request_context.request_id = request_id _request_context.request = request return None def process_response(self, request, response): """Add request ID to response headers.""" request_id = getattr(request, "request_id", None) if request_id: response[self.REQUEST_ID_HEADER] = request_id # Clean up thread-local storage _request_context.request_id = None _request_context.request = None return response def process_exception(self, request, exception): """Log exception with request ID.""" request_id = getattr(request, "request_id", "unknown") logger.error(f"[{request_id}] Unhandled exception: {exception}") return None class RequestLoggingMiddleware(MiddlewareMixin): """ Middleware for logging request/response details. Logs: - Request method, path, and request ID - Response status code and timing (if available) """ def process_request(self, request): """Log incoming request.""" import time request._start_time = time.time() request_id = getattr(request, "request_id", "N/A") logger.info( f"[{request_id}] {request.method} {request.path} - Started", extra={ "request_id": request_id, "method": request.method, "path": request.path, "user": getattr(request.user, "id", None) if hasattr(request, "user") else None, }, ) return None def process_response(self, request, response): """Log response details.""" import time duration = None if hasattr(request, "_start_time"): duration = time.time() - request._start_time request_id = getattr(request, "request_id", "N/A") logger.info( f"[{request_id}] {request.method} {request.path} - " f"{response.status_code} ({duration:.3f}s)" if duration else f"[{request_id}] {request.method} {request.path} - " f"{response.status_code}", extra={ "request_id": request_id, "method": request.method, "path": request.path, "status_code": response.status_code, "duration": duration, }, ) return response