Files
BanGUI/backend/app/middleware/correlation.py
Lukas 7ec80fdeec refactor(logging): replace structlog with stdlib logging compat layer
- 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
2026-05-10 13:37:54 +02:00

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