Files
thisORthat/app/middleware.py
T
2026-09-01 11:24:43 +03:00

70 lines
2.2 KiB
Python

"""
HTTP request/response logging middleware for FastAPI.
Normal requests (2xx/3xx):
INFO — method, path, query_params, status_code, duration_ms
Error responses (4xx):
WARNING — all above + request_body (truncated to 1000 chars)
Server errors (5xx):
ERROR — all above + request_body (truncated to 1000 chars)
"""
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from logger import logger
_BODY_METHODS = frozenset({"POST", "PUT", "PATCH"})
_BODY_MAX_LEN = 1000
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
start = time.perf_counter()
# Read body only for methods that carry a payload
body: str | None = None
if request.method in _BODY_METHODS:
raw = await request.body()
body = raw.decode(errors="replace")[:_BODY_MAX_LEN]
response = await call_next(request)
duration_ms = round((time.perf_counter() - start) * 1000, 1)
status = response.status_code
client_ip = request.headers.get("X-Forwarded-For")
if client_ip:
client_ip = client_ip.split(",")[0].strip()
else:
client_ip = request.headers.get("X-Real-IP") or (request.client.host if request.client else "unknown")
base_fields = {
"method": request.method,
"path": request.url.path,
"query": str(request.query_params) or None,
"client_ip": client_ip,
"status": status,
"duration_ms": duration_ms,
}
if status >= 500:
logger.bind(**base_fields, request_body=body).error(
"{method} {path}{status} ({duration_ms}ms)", **base_fields
)
elif status >= 400:
logger.bind(**base_fields, request_body=body).warning(
"{method} {path}{status} ({duration_ms}ms)", **base_fields
)
else:
logger.bind(**base_fields).info(
"{method} {path}{status} ({duration_ms}ms)", **base_fields
)
return response