DateTime Timezone Handling (Critical Bug Fix): - Updated all models to use DateTime(timezone=True) for PostgreSQL - Changed datetime defaults to lambda: datetime.now(timezone.utc) - Fixed mixing of timezone-aware and timezone-naive datetime objects - Resolved Internal Server Errors in session completion endpoint - Affected models: User, Team, Tree, Session, Attachment Production Logging System: - Created logging_config.py with structured logging setup - Added log rotation (10MB files, 10 backups) for production - Implemented RequestLoggingMiddleware with correlation IDs - Added ErrorLoggingMiddleware for comprehensive error tracking - Integrated logging into main.py application startup - Supports dev/prod modes with appropriate log levels Integration Tests - Session Workflow: - Created test_sessions.py with 12 comprehensive tests - Session lifecycle: create, update, complete - Session export in multiple formats (markdown, text, HTML) - Error handling and authorization checks - Added pytest.ini with coverage configuration - Added requirements-dev.txt with pytest dependencies Following 2026 FastAPI best practices for timezone handling and structured logging. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
119 lines
3.5 KiB
Python
119 lines
3.5 KiB
Python
"""
|
|
Middleware for request logging and tracking.
|
|
|
|
Implements correlation ID tracking for requests and comprehensive logging
|
|
following 2026 FastAPI best practices.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
import uuid
|
|
from typing import Callable
|
|
|
|
from fastapi import Request, Response
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.types import ASGIApp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware to log all HTTP requests with timing and correlation IDs.
|
|
|
|
Features:
|
|
- Generates unique correlation ID for each request
|
|
- Logs request method, path, and client IP
|
|
- Measures and logs request processing time
|
|
- Logs response status code
|
|
- Adds correlation ID to response headers for tracing
|
|
"""
|
|
|
|
async def dispatch(
|
|
self, request: Request, call_next: Callable
|
|
) -> Response:
|
|
# Generate correlation ID for request tracking
|
|
correlation_id = str(uuid.uuid4())
|
|
request.state.correlation_id = correlation_id
|
|
|
|
# Get client IP
|
|
client_host = request.client.host if request.client else "unknown"
|
|
|
|
# Log incoming request
|
|
logger.info(
|
|
f"Request started - "
|
|
f"method={request.method} "
|
|
f"path={request.url.path} "
|
|
f"client={client_host} "
|
|
f"correlation_id={correlation_id}"
|
|
)
|
|
|
|
# Process request and measure time
|
|
start_time = time.time()
|
|
|
|
try:
|
|
response = await call_next(request)
|
|
process_time = time.time() - start_time
|
|
|
|
# Add correlation ID to response headers
|
|
response.headers["X-Correlation-ID"] = correlation_id
|
|
response.headers["X-Process-Time"] = str(process_time)
|
|
|
|
# Log response
|
|
logger.info(
|
|
f"Request completed - "
|
|
f"method={request.method} "
|
|
f"path={request.url.path} "
|
|
f"status={response.status_code} "
|
|
f"duration={process_time:.3f}s "
|
|
f"correlation_id={correlation_id}"
|
|
)
|
|
|
|
return response
|
|
|
|
except Exception as exc:
|
|
process_time = time.time() - start_time
|
|
|
|
# Log error
|
|
logger.error(
|
|
f"Request failed - "
|
|
f"method={request.method} "
|
|
f"path={request.url.path} "
|
|
f"duration={process_time:.3f}s "
|
|
f"correlation_id={correlation_id} "
|
|
f"error={str(exc)}",
|
|
exc_info=True
|
|
)
|
|
|
|
# Re-raise exception to be handled by FastAPI
|
|
raise
|
|
|
|
|
|
class ErrorLoggingMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware to catch and log unhandled exceptions.
|
|
|
|
Ensures all exceptions are logged before being returned to the client,
|
|
providing full stack traces for debugging.
|
|
"""
|
|
|
|
async def dispatch(
|
|
self, request: Request, call_next: Callable
|
|
) -> Response:
|
|
try:
|
|
response = await call_next(request)
|
|
return response
|
|
except Exception as exc:
|
|
correlation_id = getattr(request.state, "correlation_id", "unknown")
|
|
|
|
logger.error(
|
|
f"Unhandled exception - "
|
|
f"method={request.method} "
|
|
f"path={request.url.path} "
|
|
f"correlation_id={correlation_id}",
|
|
exc_info=True
|
|
)
|
|
|
|
# Re-raise to let FastAPI handle the response
|
|
raise
|