85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
from secrets import token_urlsafe
|
|
import logging
|
|
import time
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
|
|
from app.logging_context import set_request_id
|
|
|
|
REQUEST_ID_HEADER = "X-Request-ID"
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
def __init__(
|
|
self,
|
|
app,
|
|
*,
|
|
log_requests: bool = True,
|
|
skip_paths: tuple[str, ...] = (),
|
|
) -> None:
|
|
super().__init__(app)
|
|
self._log_requests = log_requests
|
|
self._skip_paths = tuple(path for path in skip_paths if path)
|
|
|
|
async def dispatch(self, request: Request, call_next) -> Response:
|
|
request_id = request.headers.get(REQUEST_ID_HEADER) or token_urlsafe(12)
|
|
request.state.request_id = request_id
|
|
set_request_id(request_id)
|
|
|
|
start = time.perf_counter()
|
|
should_log = self._log_requests and not self._is_skipped_path(request.url.path)
|
|
if should_log:
|
|
logger.info(
|
|
"request.started",
|
|
extra={
|
|
"event": "request.started",
|
|
"method": request.method,
|
|
"path": request.url.path,
|
|
},
|
|
)
|
|
|
|
try:
|
|
response = await call_next(request)
|
|
except Exception:
|
|
duration_ms = int((time.perf_counter() - start) * 1000)
|
|
logger.exception(
|
|
"request.failed",
|
|
extra={
|
|
"event": "request.failed",
|
|
"method": request.method,
|
|
"path": request.url.path,
|
|
"duration_ms": duration_ms,
|
|
},
|
|
)
|
|
raise
|
|
else:
|
|
duration_ms = int((time.perf_counter() - start) * 1000)
|
|
response.headers[REQUEST_ID_HEADER] = request_id
|
|
if should_log:
|
|
logger.info(
|
|
"request.completed",
|
|
extra={
|
|
"event": "request.completed",
|
|
"method": request.method,
|
|
"path": request.url.path,
|
|
"status_code": response.status_code,
|
|
"duration_ms": duration_ms,
|
|
},
|
|
)
|
|
return response
|
|
finally:
|
|
set_request_id(None)
|
|
|
|
def _is_skipped_path(self, path: str) -> bool:
|
|
return any(path.startswith(prefix) for prefix in self._skip_paths)
|
|
|
|
|
|
def parse_skip_paths(raw_value: str) -> tuple[str, ...]:
|
|
parts = [part.strip() for part in raw_value.split(",")]
|
|
return tuple(part for part in parts if part)
|