Backend logging: - Centralized JSON logging config with LOG_LEVEL env var - Request logging middleware: user, method, path, status, duration, request_id - Fixed all 9 silent except:pass blocks to log warnings with tracebacks - Celery workers use same structured JSON format Infrastructure: - Loki 3.3.2 for log storage (30-day retention) - Promtail 3.3.2 for Docker container log shipping - Grafana 10.3.1 with auto-provisioned Loki datasource - Grafana on port 3002 (admin/pedshub_grafana) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""Middleware that logs every HTTP request with user context and timing."""
|
|
import logging
|
|
import time
|
|
import uuid
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
|
|
logger = logging.getLogger("pedshub.requests")
|
|
|
|
# Paths to skip logging (noisy or uninteresting)
|
|
SKIP_PATHS = {"/api/health", "/favicon.ico", "/robots.txt"}
|
|
SKIP_PREFIXES = ("/uploads/",)
|
|
|
|
|
|
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
path = request.url.path
|
|
|
|
# Skip noisy paths
|
|
if path in SKIP_PATHS or any(path.startswith(p) for p in SKIP_PREFIXES):
|
|
return await call_next(request)
|
|
|
|
request_id = uuid.uuid4().hex[:8]
|
|
request.state.request_id = request_id
|
|
|
|
# Extract user from JWT (best-effort, never block)
|
|
user_id = None
|
|
user_email = None
|
|
auth = request.headers.get("authorization", "")
|
|
if auth.startswith("Bearer "):
|
|
try:
|
|
from jose import jwt
|
|
from app.config import settings
|
|
payload = jwt.decode(
|
|
auth[7:], settings.SECRET_KEY,
|
|
algorithms=[settings.ALGORITHM],
|
|
options={"verify_exp": False},
|
|
)
|
|
user_email = payload.get("sub")
|
|
except Exception:
|
|
pass
|
|
|
|
start = time.perf_counter()
|
|
response = await call_next(request)
|
|
duration_ms = round((time.perf_counter() - start) * 1000, 1)
|
|
|
|
status = response.status_code
|
|
method = request.method
|
|
client_ip = request.client.host if request.client else "-"
|
|
|
|
log_data = {
|
|
"request_id": request_id,
|
|
"method": method,
|
|
"path": path,
|
|
"status": status,
|
|
"duration_ms": duration_ms,
|
|
"client_ip": client_ip,
|
|
}
|
|
if user_email:
|
|
log_data["user"] = user_email
|
|
|
|
if status >= 500:
|
|
logger.error("%(method)s %(path)s %(status)s %(duration_ms)sms", log_data, extra=log_data)
|
|
elif status >= 400:
|
|
logger.warning("%(method)s %(path)s %(status)s %(duration_ms)sms", log_data, extra=log_data)
|
|
else:
|
|
logger.info("%(method)s %(path)s %(status)s %(duration_ms)sms", log_data, extra=log_data)
|
|
|
|
return response
|