- Split /health into /health/live (liveness) and /health/ready (readiness) following Kubernetes conventions. Combined /health retained for backward compatibility with existing Docker HEALTHCHECK definitions. - Add ReadyCheck and ReadyResponse models for structured readiness output. - Add _assert_middleware_order() startup check enforcing: RateLimit → Csrf → CorrelationId middleware chain. - Register CorrelationIdMiddleware, CsrfMiddleware, RateLimitMiddleware in create_app() with documented required order (reverse of processing). - Add correlation.py, csrf.py, rate_limit.py middleware modules. - Add health probe tests in test_health_probes.py. - Update test_main.py with middleware order assertion tests. - Update frontend useFetchData hook tests. - Docs: update Deployment.md with Kubernetes probe config examples.
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
"""Correlation ID middleware for distributed tracing.
|
|
|
|
This middleware generates or extracts a correlation ID from each request,
|
|
stores it in structlog's contextvars, 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. Middleware stores in structlog.contextvars
|
|
4. All log entries include the correlation ID automatically
|
|
5. 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
|
|
|
|
import uuid
|
|
from typing import TYPE_CHECKING
|
|
|
|
import structlog
|
|
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: structlog.stdlib.BoundLogger = structlog.get_logger()
|
|
|
|
# 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 structlog context
|
|
CORRELATION_ID_CONTEXT_KEY: str = "correlation_id"
|
|
|
|
|
|
class CorrelationIdMiddleware(BaseHTTPMiddleware):
|
|
"""Extract or generate correlation ID and inject into structlog context.
|
|
|
|
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 in structlog.contextvars so all logs for this request include it
|
|
4. Makes available via request.state for error handlers
|
|
|
|
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 in structlog context so all logs for this request include it
|
|
structlog.contextvars.clear_contextvars()
|
|
structlog.contextvars.bind_contextvars(
|
|
**{CORRELATION_ID_CONTEXT_KEY: correlation_id}
|
|
)
|
|
|
|
# Also store on request.state for use by exception handlers
|
|
request.state.correlation_id = correlation_id
|
|
|
|
log.debug(
|
|
"request_received",
|
|
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
|