pdf-quiz-generator/backend/app/middleware/request_logging.py
Daniel d0518d0737 Add comprehensive structured logging with Loki + Grafana
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>
2026-04-11 02:53:54 +02:00

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