perf(organizations): cache and instrument API responses
This commit is contained in:
@@ -4,15 +4,20 @@ Core middleware components.
|
||||
Provides Request ID tracking and other cross-cutting concerns.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from django.db import connection
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
from django.template.response import ContentNotRenderedError
|
||||
from django.urls import Resolver404, resolve
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
organization_metrics_logger = logging.getLogger("organizations.api.metrics")
|
||||
|
||||
# Thread-local storage for request context
|
||||
_request_context = threading.local()
|
||||
@@ -170,3 +175,102 @@ class RequestLoggingMiddleware(MiddlewareMixin):
|
||||
},
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class OrganizationApiMetricsMiddleware:
|
||||
"""Emit focused request metrics for API endpoints that serve organizations."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if not self._should_log(request):
|
||||
return self.get_response(request)
|
||||
|
||||
db_query_count = 0
|
||||
db_duration_seconds = 0.0
|
||||
started_at = time.perf_counter()
|
||||
response = None
|
||||
error = None
|
||||
|
||||
def execute_with_metrics(execute, sql, params, many, context):
|
||||
nonlocal db_query_count, db_duration_seconds
|
||||
|
||||
db_query_count += 1
|
||||
query_started_at = time.perf_counter()
|
||||
try:
|
||||
return execute(sql, params, many, context)
|
||||
finally:
|
||||
db_duration_seconds += time.perf_counter() - query_started_at
|
||||
|
||||
try:
|
||||
with connection.execute_wrapper(execute_with_metrics):
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
except Exception as exc:
|
||||
error = exc.__class__.__name__
|
||||
raise
|
||||
finally:
|
||||
duration_seconds = time.perf_counter() - started_at
|
||||
self._log_metrics(
|
||||
request=request,
|
||||
response=response,
|
||||
error=error,
|
||||
duration_seconds=duration_seconds,
|
||||
db_query_count=db_query_count,
|
||||
db_duration_seconds=db_duration_seconds,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _should_log(request) -> bool:
|
||||
path = getattr(request, "path_info", "") or getattr(request, "path", "")
|
||||
if not path.startswith("/api/"):
|
||||
return False
|
||||
return "organizations" in path.strip("/").split("/")
|
||||
|
||||
@classmethod
|
||||
def _log_metrics(
|
||||
cls,
|
||||
*,
|
||||
request,
|
||||
response,
|
||||
error: str | None,
|
||||
duration_seconds: float,
|
||||
db_query_count: int,
|
||||
db_duration_seconds: float,
|
||||
) -> None:
|
||||
metrics = {
|
||||
"request_id": getattr(request, "request_id", None),
|
||||
"method": request.method,
|
||||
"path": getattr(request, "path_info", "") or request.path,
|
||||
"status_code": getattr(response, "status_code", 500),
|
||||
"duration_ms": round(duration_seconds * 1000, 2),
|
||||
"db_query_count": db_query_count,
|
||||
"db_duration_ms": round(db_duration_seconds * 1000, 2),
|
||||
"response_size_bytes": cls._response_size(response),
|
||||
"cache": response.get("X-Cache") if response is not None else None,
|
||||
"user_id": cls._user_id(request),
|
||||
"query_keys": sorted(request.GET.keys()),
|
||||
"error": error,
|
||||
}
|
||||
organization_metrics_logger.info(
|
||||
"organization_api_metrics %s",
|
||||
json.dumps(metrics, ensure_ascii=False, sort_keys=True),
|
||||
extra={"organization_api_metrics": metrics},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _response_size(response) -> int | None:
|
||||
if response is None or getattr(response, "streaming", False):
|
||||
return None
|
||||
try:
|
||||
return len(response.content)
|
||||
except (AttributeError, ContentNotRenderedError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _user_id(request) -> int | None:
|
||||
user = getattr(request, "user", None)
|
||||
if user is None or not getattr(user, "is_authenticated", False):
|
||||
return None
|
||||
return getattr(user, "id", None)
|
||||
|
||||
Reference in New Issue
Block a user