feat: add Sentry, Cloudflare R2, and CloudFlare CDN integrations
Production quick wins for improved observability and scalability: Sentry Error Tracking: - Add sentry-sdk[fastapi] dependency - Initialize Sentry in main.py with FastAPI/SQLAlchemy integrations - Add Celery integration for background task error tracking - Feature-flagged via SENTRY_DSN (disabled when empty) Cloudflare R2 Storage: - Add boto3 dependency for S3-compatible API - Create storage_service.py with StorageBackend abstraction - LocalStorageBackend for development (default) - R2StorageBackend for production cloud storage - Feature-flagged via STORAGE_BACKEND setting CloudFlare CDN/Proxy: - Create middleware/cloudflare.py for CF header handling - Extract real client IP from CF-Connecting-IP - Support CF-IPCountry for geo features - Feature-flagged via CLOUDFLARE_ENABLED setting Documentation: - Add docs/deployment/cloudflare.md setup guide - Update infrastructure.md with dev vs prod requirements - Add enterprise upgrade checklist for scaling beyond 1000 users - Update installation.md with new environment variables All features are optional and disabled by default for development. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
149
middleware/cloudflare.py
Normal file
149
middleware/cloudflare.py
Normal file
@@ -0,0 +1,149 @@
|
||||
# middleware/cloudflare.py
|
||||
"""
|
||||
CloudFlare proxy middleware.
|
||||
|
||||
When enabled, this middleware handles CloudFlare-specific headers:
|
||||
- CF-Connecting-IP: Real client IP address
|
||||
- CF-IPCountry: Client's country code
|
||||
- CF-Ray: CloudFlare Ray ID for request tracing
|
||||
- CF-Visitor: Original request scheme
|
||||
|
||||
Usage:
|
||||
Enable by setting CLOUDFLARE_ENABLED=true in .env
|
||||
|
||||
The middleware will:
|
||||
1. Extract real client IP from CF-Connecting-IP header
|
||||
2. Store country code in request.state.client_country
|
||||
3. Log CF-Ray for request correlation
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudFlareMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware to handle CloudFlare proxy headers.
|
||||
|
||||
Only active when CLOUDFLARE_ENABLED=true.
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
"""Process request with CloudFlare headers."""
|
||||
|
||||
if not settings.cloudflare_enabled:
|
||||
return await call_next(request)
|
||||
|
||||
# Extract CloudFlare headers
|
||||
cf_connecting_ip = request.headers.get("CF-Connecting-IP")
|
||||
cf_country = request.headers.get("CF-IPCountry")
|
||||
cf_ray = request.headers.get("CF-Ray")
|
||||
cf_visitor = request.headers.get("CF-Visitor") # {"scheme":"https"}
|
||||
|
||||
# Store real client IP
|
||||
if cf_connecting_ip:
|
||||
# Override the client host with real IP
|
||||
# Note: Starlette's request.client is immutable, so we store in state
|
||||
request.state.real_ip = cf_connecting_ip
|
||||
else:
|
||||
# Fallback to standard client IP
|
||||
request.state.real_ip = (
|
||||
request.client.host if request.client else "unknown"
|
||||
)
|
||||
|
||||
# Store country code for geo features
|
||||
if cf_country:
|
||||
request.state.client_country = cf_country
|
||||
|
||||
# Store Ray ID for request tracing
|
||||
if cf_ray:
|
||||
request.state.cf_ray = cf_ray
|
||||
|
||||
# Determine original scheme
|
||||
if cf_visitor:
|
||||
try:
|
||||
import json
|
||||
visitor_info = json.loads(cf_visitor)
|
||||
request.state.original_scheme = visitor_info.get("scheme", "https")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
request.state.original_scheme = "https"
|
||||
|
||||
# Log request with CloudFlare context
|
||||
logger.debug(
|
||||
f"CloudFlare request: ip={cf_connecting_ip}, "
|
||||
f"country={cf_country}, ray={cf_ray}"
|
||||
)
|
||||
|
||||
# Process request
|
||||
response = await call_next(request)
|
||||
|
||||
# Add CF-Ray to response for debugging
|
||||
if cf_ray:
|
||||
response.headers["X-CF-Ray"] = cf_ray
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def get_real_client_ip(request: Request) -> str:
|
||||
"""
|
||||
Get the real client IP address.
|
||||
|
||||
When behind CloudFlare, this returns CF-Connecting-IP.
|
||||
Otherwise, returns the standard client IP.
|
||||
|
||||
Args:
|
||||
request: Starlette Request object
|
||||
|
||||
Returns:
|
||||
Client IP address string
|
||||
"""
|
||||
# First check if CloudFlare middleware set real_ip
|
||||
if hasattr(request.state, "real_ip"):
|
||||
return request.state.real_ip
|
||||
|
||||
# Check for CF-Connecting-IP header directly
|
||||
cf_ip = request.headers.get("CF-Connecting-IP")
|
||||
if cf_ip:
|
||||
return cf_ip
|
||||
|
||||
# Check for X-Forwarded-For (generic proxy header)
|
||||
forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if forwarded_for:
|
||||
# Take the first IP (client IP)
|
||||
return forwarded_for.split(",")[0].strip()
|
||||
|
||||
# Fallback to request client
|
||||
return request.client.host if request.client else "unknown"
|
||||
|
||||
|
||||
def get_client_country(request: Request) -> str | None:
|
||||
"""
|
||||
Get the client's country code from CloudFlare.
|
||||
|
||||
Args:
|
||||
request: Starlette Request object
|
||||
|
||||
Returns:
|
||||
ISO 3166-1 alpha-2 country code, or None if not available
|
||||
"""
|
||||
if hasattr(request.state, "client_country"):
|
||||
return request.state.client_country
|
||||
|
||||
return request.headers.get("CF-IPCountry")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PUBLIC API
|
||||
# =============================================================================
|
||||
__all__ = [
|
||||
"CloudFlareMiddleware",
|
||||
"get_real_client_ip",
|
||||
"get_client_country",
|
||||
]
|
||||
Reference in New Issue
Block a user