Align frontend and backend error observability with correlation IDs and structured telemetry for distributed tracing across systems. Backend changes: - Add CorrelationIdMiddleware to generate/extract correlation IDs - Include correlation_id in all ErrorResponse objects - Store correlation ID in structlog contextvars for automatic inclusion in logs - Add correlation ID to response headers (X-Correlation-ID) Frontend changes: - API client automatically generates session-scoped UUID4 and includes X-Correlation-ID header in all requests - Extract correlation ID from API error responses - Update error handlers to use telemetry with correlation IDs - Add telemetry logging to ErrorBoundary, PageErrorBoundary, SectionErrorBoundary - Implement redaction utilities for privacy-safe logging of sensitive data Documentation: - Add observability guidelines to Web-Development.md * Correlation ID usage patterns * Privacy & security best practices * Telemetry event structure * Redaction utilities for sensitive data - Add distributed tracing architecture section to Architecture.md * Correlation ID flow across frontend/backend * Example troubleshooting scenario * Implementation details for future enhancements Testing: - Add comprehensive tests for correlation middleware - Update error boundary tests to verify telemetry integration - Verify TypeScript and ESLint pass with no warnings Fixes: Issue #40 - Frontend and backend observability are not aligned Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
94 lines
3.3 KiB
Python
94 lines
3.3 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
|
|
"""
|
|
|
|
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
|