- Remove structlog dependency from backend/pyproject.toml - Add app.utils.logging_compat shim for keyword-arg logging API - Add app.utils.json_formatter for JSON log output with extra fields - Update all backend modules to use logging_compat.get_logger() - Update docstrings in log_sanitizer.py and json_formatter.py - Update test comment in test_async_utils.py - Record 406 failing tests in Docs/Tasks.md for tracking
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Correlation ID middleware for distributed tracing.
|
|
|
|
This middleware generates or extracts a correlation ID from each request,
|
|
stores it in request state, and includes it in error responses.
|
|
This enables correlating logs across frontend and backend for a single
|
|
user action or request flow.
|
|
|
|
Correlation IDs flow through the request lifecycle:
|
|
1. Frontend generates/passes via `X-Correlation-ID` header
|
|
2. Middleware extracts or generates a UUID4
|
|
3. Stores on request.state for use by error handlers and log filters
|
|
4. Error responses include the correlation ID for client-side correlation
|
|
|
|
Processing order
|
|
-----------------
|
|
This middleware must be the outermost in the security-critical chain so it
|
|
executes first on incoming requests (outermost = first to see request,
|
|
last to see response). In the required chain:
|
|
|
|
CorrelationIdMiddleware → CsrfMiddleware → RateLimitMiddleware
|
|
|
|
The registration order in ``main.py`` must be:
|
|
RateLimitMiddleware, CsrfMiddleware, CorrelationIdMiddleware
|
|
(last registered = outermost in Starlette's reverse application).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.utils.logging_compat import get_logger
|
|
import uuid
|
|
from typing import TYPE_CHECKING
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response as StarletteResponse
|
|
|
|
log = get_logger(__name__)
|
|
|
|
# Standard header name for correlation IDs (follows W3C Trace Context conventions)
|
|
_CORRELATION_ID_HEADER: str = "X-Correlation-ID"
|
|
|
|
# Key name for storing correlation ID in request state
|
|
CORRELATION_ID_CONTEXT_KEY: str = "correlation_id"
|
|
|
|
|
|
class CorrelationIdMiddleware(BaseHTTPMiddleware):
|
|
"""Extract or generate correlation ID and store on request state.
|
|
|
|
For each request, this middleware:
|
|
1. Checks for `X-Correlation-ID` header (trusted from frontend)
|
|
2. Generates a new UUID4 if header not present
|
|
3. Stores on request.state for use by error handlers and log filters
|
|
|
|
The correlation ID enables tracing a single user action or request flow
|
|
across both frontend and backend systems using structured logs.
|
|
"""
|
|
|
|
async def dispatch(
|
|
self,
|
|
request: Request,
|
|
call_next: Callable[[Request], Awaitable[StarletteResponse]],
|
|
) -> StarletteResponse:
|
|
"""Intercept requests to extract or generate correlation ID.
|
|
|
|
Args:
|
|
request: The incoming HTTP request.
|
|
call_next: The next middleware / router handler.
|
|
|
|
Returns:
|
|
The response from the next middleware / router, with correlation ID
|
|
in the request state for use by exception handlers.
|
|
"""
|
|
# Extract correlation ID from request header, or generate a new one
|
|
correlation_id: str = request.headers.get(
|
|
_CORRELATION_ID_HEADER,
|
|
str(uuid.uuid4()),
|
|
)
|
|
|
|
# Store on request.state for use by exception handlers
|
|
request.state.correlation_id = correlation_id
|
|
|
|
log.debug(
|
|
"request_received",
|
|
extra={"method": request.method, "path": request.url.path},
|
|
)
|
|
|
|
response: StarletteResponse = await call_next(request)
|
|
|
|
# Add correlation ID to response header so frontend can correlate errors
|
|
response.headers[_CORRELATION_ID_HEADER] = correlation_id
|
|
|
|
return response
|