- Add Model Mixins: TimestampMixin, SoftDeleteMixin, AuditMixin, etc. - Add Base Services: BaseService, BulkOperationsMixin, QueryOptimizerMixin - Add Base ViewSets with bulk operations - Add BackgroundJob model for Celery task tracking - Add BaseAppCommand for management commands - Add permissions, pagination, filters, cache, logging - Migrate tests to factory_boy + faker - Add CHANGELOG.md - 297 tests passing
132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
"""
|
|
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
|