mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 07:11:08 -05:00
Refactor test utilities and enhance ASGI settings
- Cleaned up and standardized assertions in ApiTestMixin for API response validation. - Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE. - Removed unused imports and improved formatting in settings.py. - Refactored URL patterns in urls.py for better readability and organization. - Enhanced view functions in views.py for consistency and clarity. - Added .flake8 configuration for linting and style enforcement. - Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
@@ -8,15 +8,15 @@ from .performance_middleware import (
|
||||
PerformanceMiddleware,
|
||||
QueryCountMiddleware,
|
||||
DatabaseConnectionMiddleware,
|
||||
CachePerformanceMiddleware
|
||||
CachePerformanceMiddleware,
|
||||
)
|
||||
|
||||
# Make all middleware classes available at the package level
|
||||
__all__ = [
|
||||
'PageViewMiddleware',
|
||||
'PgHistoryContextMiddleware',
|
||||
'PerformanceMiddleware',
|
||||
'QueryCountMiddleware',
|
||||
'DatabaseConnectionMiddleware',
|
||||
'CachePerformanceMiddleware'
|
||||
"PageViewMiddleware",
|
||||
"PgHistoryContextMiddleware",
|
||||
"PerformanceMiddleware",
|
||||
"QueryCountMiddleware",
|
||||
"DatabaseConnectionMiddleware",
|
||||
"CachePerformanceMiddleware",
|
||||
]
|
||||
|
||||
@@ -13,12 +13,19 @@ from core.analytics import PageView
|
||||
|
||||
class RequestContextProvider(pghistory.context):
|
||||
"""Custom context provider for pghistory that extracts information from the request."""
|
||||
|
||||
def __call__(self, request: WSGIRequest) -> dict:
|
||||
return {
|
||||
'user': str(request.user) if request.user and not isinstance(request.user, AnonymousUser) else None,
|
||||
'ip': request.META.get('REMOTE_ADDR'),
|
||||
'user_agent': request.META.get('HTTP_USER_AGENT'),
|
||||
'session_key': request.session.session_key if hasattr(request, 'session') else None
|
||||
"user": (
|
||||
str(request.user)
|
||||
if request.user and not isinstance(request.user, AnonymousUser)
|
||||
else None
|
||||
),
|
||||
"ip": request.META.get("REMOTE_ADDR"),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||
"session_key": (
|
||||
request.session.session_key if hasattr(request, "session") else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +37,7 @@ class PgHistoryContextMiddleware:
|
||||
"""
|
||||
Middleware that ensures request object is available to pghistory context.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@@ -40,14 +48,14 @@ class PgHistoryContextMiddleware:
|
||||
|
||||
class PageViewMiddleware(MiddlewareMixin):
|
||||
"""Middleware to track page views for DetailView-based pages."""
|
||||
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
# Only track GET requests
|
||||
if request.method != 'GET':
|
||||
if request.method != "GET":
|
||||
return None
|
||||
|
||||
# Get view class if it exists
|
||||
view_class = getattr(view_func, 'view_class', None)
|
||||
view_class = getattr(view_func, "view_class", None)
|
||||
if not view_class or not issubclass(view_class, DetailView):
|
||||
return None
|
||||
|
||||
@@ -66,8 +74,8 @@ class PageViewMiddleware(MiddlewareMixin):
|
||||
PageView.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(obj.__class__),
|
||||
object_id=obj.pk,
|
||||
ip_address=request.META.get('REMOTE_ADDR', ''),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', '')[:512]
|
||||
ip_address=request.META.get("REMOTE_ADDR", ""),
|
||||
user_agent=request.META.get("HTTP_USER_AGENT", "")[:512],
|
||||
)
|
||||
except Exception:
|
||||
# Fail silently to not interrupt the request
|
||||
|
||||
@@ -8,131 +8,169 @@ from django.db import connection
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.conf import settings
|
||||
|
||||
performance_logger = logging.getLogger('performance')
|
||||
performance_logger = logging.getLogger("performance")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PerformanceMiddleware(MiddlewareMixin):
|
||||
"""Middleware to collect performance metrics for each request"""
|
||||
|
||||
|
||||
def process_request(self, request):
|
||||
"""Initialize performance tracking for the request"""
|
||||
request._performance_start_time = time.time()
|
||||
request._performance_initial_queries = len(connection.queries) if hasattr(connection, 'queries') else 0
|
||||
request._performance_initial_queries = (
|
||||
len(connection.queries) if hasattr(connection, "queries") else 0
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Log performance metrics after response is ready"""
|
||||
# Skip performance tracking for certain paths
|
||||
skip_paths = ['/health/', '/admin/jsi18n/', '/static/', '/media/', '/__debug__/']
|
||||
skip_paths = [
|
||||
"/health/",
|
||||
"/admin/jsi18n/",
|
||||
"/static/",
|
||||
"/media/",
|
||||
"/__debug__/",
|
||||
]
|
||||
if any(request.path.startswith(path) for path in skip_paths):
|
||||
return response
|
||||
|
||||
|
||||
# Calculate metrics
|
||||
end_time = time.time()
|
||||
start_time = getattr(request, '_performance_start_time', end_time)
|
||||
start_time = getattr(request, "_performance_start_time", end_time)
|
||||
duration = end_time - start_time
|
||||
|
||||
initial_queries = getattr(request, '_performance_initial_queries', 0)
|
||||
total_queries = len(connection.queries) - initial_queries if hasattr(connection, 'queries') else 0
|
||||
|
||||
|
||||
initial_queries = getattr(request, "_performance_initial_queries", 0)
|
||||
total_queries = (
|
||||
len(connection.queries) - initial_queries
|
||||
if hasattr(connection, "queries")
|
||||
else 0
|
||||
)
|
||||
|
||||
# Get content length
|
||||
content_length = 0
|
||||
if hasattr(response, 'content'):
|
||||
if hasattr(response, "content"):
|
||||
content_length = len(response.content)
|
||||
elif hasattr(response, 'streaming_content'):
|
||||
elif hasattr(response, "streaming_content"):
|
||||
# For streaming responses, we can't easily measure content length
|
||||
content_length = -1
|
||||
|
||||
|
||||
# Build performance data
|
||||
performance_data = {
|
||||
'path': request.path,
|
||||
'method': request.method,
|
||||
'status_code': response.status_code,
|
||||
'duration_ms': round(duration * 1000, 2),
|
||||
'duration_seconds': round(duration, 3),
|
||||
'query_count': total_queries,
|
||||
'content_length_bytes': content_length,
|
||||
'user_id': getattr(request.user, 'id', None) if hasattr(request, 'user') and request.user.is_authenticated else None,
|
||||
'user_agent': request.META.get('HTTP_USER_AGENT', '')[:100], # Truncate user agent
|
||||
'remote_addr': self._get_client_ip(request),
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"status_code": response.status_code,
|
||||
"duration_ms": round(duration * 1000, 2),
|
||||
"duration_seconds": round(duration, 3),
|
||||
"query_count": total_queries,
|
||||
"content_length_bytes": content_length,
|
||||
"user_id": (
|
||||
getattr(request.user, "id", None)
|
||||
if hasattr(request, "user") and request.user.is_authenticated
|
||||
else None
|
||||
),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT", "")[
|
||||
:100
|
||||
], # Truncate user agent
|
||||
"remote_addr": self._get_client_ip(request),
|
||||
}
|
||||
|
||||
|
||||
# Add query details in debug mode
|
||||
if settings.DEBUG and hasattr(connection, 'queries') and total_queries > 0:
|
||||
if settings.DEBUG and hasattr(connection, "queries") and total_queries > 0:
|
||||
recent_queries = connection.queries[-total_queries:]
|
||||
performance_data['queries'] = [
|
||||
performance_data["queries"] = [
|
||||
{
|
||||
'sql': query['sql'][:200] + '...' if len(query['sql']) > 200 else query['sql'],
|
||||
'time': float(query['time'])
|
||||
"sql": (
|
||||
query["sql"][:200] + "..."
|
||||
if len(query["sql"]) > 200
|
||||
else query["sql"]
|
||||
),
|
||||
"time": float(query["time"]),
|
||||
}
|
||||
for query in recent_queries[-10:] # Last 10 queries only
|
||||
]
|
||||
|
||||
|
||||
# Identify slow queries
|
||||
slow_queries = [q for q in recent_queries if float(q['time']) > 0.1]
|
||||
slow_queries = [q for q in recent_queries if float(q["time"]) > 0.1]
|
||||
if slow_queries:
|
||||
performance_data['slow_query_count'] = len(slow_queries)
|
||||
performance_data['slowest_query_time'] = max(float(q['time']) for q in slow_queries)
|
||||
|
||||
performance_data["slow_query_count"] = len(slow_queries)
|
||||
performance_data["slowest_query_time"] = max(
|
||||
float(q["time"]) for q in slow_queries
|
||||
)
|
||||
|
||||
# Determine log level based on performance
|
||||
log_level = self._get_log_level(duration, total_queries, response.status_code)
|
||||
|
||||
|
||||
# Log the performance data
|
||||
performance_logger.log(
|
||||
log_level,
|
||||
f"Request performance: {request.method} {request.path} - "
|
||||
f"{duration:.3f}s, {total_queries} queries, {response.status_code}",
|
||||
extra=performance_data
|
||||
extra=performance_data,
|
||||
)
|
||||
|
||||
|
||||
# Add performance headers for debugging (only in debug mode)
|
||||
if settings.DEBUG:
|
||||
response['X-Response-Time'] = f"{duration * 1000:.2f}ms"
|
||||
response['X-Query-Count'] = str(total_queries)
|
||||
if total_queries > 0 and hasattr(connection, 'queries'):
|
||||
total_query_time = sum(float(q['time']) for q in connection.queries[-total_queries:])
|
||||
response['X-Query-Time'] = f"{total_query_time * 1000:.2f}ms"
|
||||
|
||||
response["X-Response-Time"] = f"{duration * 1000:.2f}ms"
|
||||
response["X-Query-Count"] = str(total_queries)
|
||||
if total_queries > 0 and hasattr(connection, "queries"):
|
||||
total_query_time = sum(
|
||||
float(q["time"]) for q in connection.queries[-total_queries:]
|
||||
)
|
||||
response["X-Query-Time"] = f"{total_query_time * 1000:.2f}ms"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
"""Log performance data even when an exception occurs"""
|
||||
end_time = time.time()
|
||||
start_time = getattr(request, '_performance_start_time', end_time)
|
||||
start_time = getattr(request, "_performance_start_time", end_time)
|
||||
duration = end_time - start_time
|
||||
|
||||
initial_queries = getattr(request, '_performance_initial_queries', 0)
|
||||
total_queries = len(connection.queries) - initial_queries if hasattr(connection, 'queries') else 0
|
||||
|
||||
performance_data = {
|
||||
'path': request.path,
|
||||
'method': request.method,
|
||||
'status_code': 500, # Exception occurred
|
||||
'duration_ms': round(duration * 1000, 2),
|
||||
'query_count': total_queries,
|
||||
'exception': str(exception),
|
||||
'exception_type': type(exception).__name__,
|
||||
'user_id': getattr(request.user, 'id', None) if hasattr(request, 'user') and request.user.is_authenticated else None,
|
||||
}
|
||||
|
||||
performance_logger.error(
|
||||
f"Request exception: {request.method} {request.path} - "
|
||||
f"{duration:.3f}s, {total_queries} queries, {type(exception).__name__}: {exception}",
|
||||
extra=performance_data
|
||||
|
||||
initial_queries = getattr(request, "_performance_initial_queries", 0)
|
||||
total_queries = (
|
||||
len(connection.queries) - initial_queries
|
||||
if hasattr(connection, "queries")
|
||||
else 0
|
||||
)
|
||||
|
||||
|
||||
performance_data = {
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"status_code": 500, # Exception occurred
|
||||
"duration_ms": round(duration * 1000, 2),
|
||||
"query_count": total_queries,
|
||||
"exception": str(exception),
|
||||
"exception_type": type(exception).__name__,
|
||||
"user_id": (
|
||||
getattr(request.user, "id", None)
|
||||
if hasattr(request, "user") and request.user.is_authenticated
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
performance_logger.error(
|
||||
f"Request exception: {
|
||||
request.method} {
|
||||
request.path} - "
|
||||
f"{
|
||||
duration:.3f}s, {total_queries} queries, {
|
||||
type(exception).__name__}: {exception}",
|
||||
extra=performance_data,
|
||||
)
|
||||
|
||||
return None # Don't handle the exception, just log it
|
||||
|
||||
|
||||
def _get_client_ip(self, request):
|
||||
"""Extract client IP address from request"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0].strip()
|
||||
ip = x_forwarded_for.split(",")[0].strip()
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR', '')
|
||||
ip = request.META.get("REMOTE_ADDR", "")
|
||||
return ip
|
||||
|
||||
|
||||
def _get_log_level(self, duration, query_count, status_code):
|
||||
"""Determine appropriate log level based on performance metrics"""
|
||||
# Error responses
|
||||
@@ -140,7 +178,7 @@ class PerformanceMiddleware(MiddlewareMixin):
|
||||
return logging.ERROR
|
||||
elif status_code >= 400:
|
||||
return logging.WARNING
|
||||
|
||||
|
||||
# Performance-based log levels
|
||||
if duration > 5.0: # Very slow requests
|
||||
return logging.ERROR
|
||||
@@ -154,50 +192,55 @@ class PerformanceMiddleware(MiddlewareMixin):
|
||||
|
||||
class QueryCountMiddleware(MiddlewareMixin):
|
||||
"""Middleware to track and limit query counts per request"""
|
||||
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
self.query_limit = getattr(settings, 'MAX_QUERIES_PER_REQUEST', 50)
|
||||
self.query_limit = getattr(settings, "MAX_QUERIES_PER_REQUEST", 50)
|
||||
super().__init__(get_response)
|
||||
|
||||
|
||||
def process_request(self, request):
|
||||
"""Initialize query tracking"""
|
||||
request._query_count_start = len(connection.queries) if hasattr(connection, 'queries') else 0
|
||||
request._query_count_start = (
|
||||
len(connection.queries) if hasattr(connection, "queries") else 0
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Check query count and warn if excessive"""
|
||||
if not hasattr(connection, 'queries'):
|
||||
if not hasattr(connection, "queries"):
|
||||
return response
|
||||
|
||||
start_count = getattr(request, '_query_count_start', 0)
|
||||
|
||||
start_count = getattr(request, "_query_count_start", 0)
|
||||
current_count = len(connection.queries)
|
||||
request_query_count = current_count - start_count
|
||||
|
||||
|
||||
if request_query_count > self.query_limit:
|
||||
logger.warning(
|
||||
f"Excessive query count: {request.path} executed {request_query_count} queries "
|
||||
f"(limit: {self.query_limit})",
|
||||
f"Excessive query count: {
|
||||
request.path} executed {request_query_count} queries "
|
||||
f"(limit: {
|
||||
self.query_limit})",
|
||||
extra={
|
||||
'path': request.path,
|
||||
'method': request.method,
|
||||
'query_count': request_query_count,
|
||||
'query_limit': self.query_limit,
|
||||
'excessive_queries': True
|
||||
}
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"query_count": request_query_count,
|
||||
"query_limit": self.query_limit,
|
||||
"excessive_queries": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class DatabaseConnectionMiddleware(MiddlewareMixin):
|
||||
"""Middleware to monitor database connection health"""
|
||||
|
||||
|
||||
def process_request(self, request):
|
||||
"""Check database connection at start of request"""
|
||||
try:
|
||||
# Simple connection test
|
||||
from django.db import connection
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.fetchone()
|
||||
@@ -205,64 +248,70 @@ class DatabaseConnectionMiddleware(MiddlewareMixin):
|
||||
logger.error(
|
||||
f"Database connection failed at request start: {e}",
|
||||
extra={
|
||||
'path': request.path,
|
||||
'method': request.method,
|
||||
'database_error': str(e)
|
||||
}
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"database_error": str(e),
|
||||
},
|
||||
)
|
||||
# Don't block the request, let Django handle the database error
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Close database connections properly"""
|
||||
try:
|
||||
from django.db import connection
|
||||
|
||||
connection.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing database connection: {e}")
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class CachePerformanceMiddleware(MiddlewareMixin):
|
||||
"""Middleware to monitor cache performance"""
|
||||
|
||||
|
||||
def process_request(self, request):
|
||||
"""Initialize cache performance tracking"""
|
||||
request._cache_hits = 0
|
||||
request._cache_misses = 0
|
||||
request._cache_start_time = time.time()
|
||||
return None
|
||||
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Log cache performance metrics"""
|
||||
cache_duration = time.time() - getattr(request, '_cache_start_time', time.time())
|
||||
cache_hits = getattr(request, '_cache_hits', 0)
|
||||
cache_misses = getattr(request, '_cache_misses', 0)
|
||||
|
||||
cache_duration = time.time() - getattr(
|
||||
request, "_cache_start_time", time.time()
|
||||
)
|
||||
cache_hits = getattr(request, "_cache_hits", 0)
|
||||
cache_misses = getattr(request, "_cache_misses", 0)
|
||||
|
||||
if cache_hits + cache_misses > 0:
|
||||
hit_rate = (cache_hits / (cache_hits + cache_misses)) * 100
|
||||
|
||||
|
||||
cache_data = {
|
||||
'path': request.path,
|
||||
'cache_hits': cache_hits,
|
||||
'cache_misses': cache_misses,
|
||||
'cache_hit_rate': round(hit_rate, 2),
|
||||
'cache_operations': cache_hits + cache_misses,
|
||||
'cache_duration': round(cache_duration * 1000, 2) # milliseconds
|
||||
"path": request.path,
|
||||
"cache_hits": cache_hits,
|
||||
"cache_misses": cache_misses,
|
||||
"cache_hit_rate": round(hit_rate, 2),
|
||||
"cache_operations": cache_hits + cache_misses,
|
||||
# milliseconds
|
||||
"cache_duration": round(cache_duration * 1000, 2),
|
||||
}
|
||||
|
||||
|
||||
# Log cache performance
|
||||
if hit_rate < 50 and cache_hits + cache_misses > 5:
|
||||
logger.warning(
|
||||
f"Low cache hit rate for {request.path}: {hit_rate:.1f}%",
|
||||
extra=cache_data
|
||||
extra=cache_data,
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Cache performance for {request.path}: {hit_rate:.1f}% hit rate",
|
||||
extra=cache_data
|
||||
f"Cache performance for {
|
||||
request.path}: {
|
||||
hit_rate:.1f}% hit rate",
|
||||
extra=cache_data,
|
||||
)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user