Files
kennethreitz 08247c2b1d Drop DbRetryMiddleware, make logger async, bump django-bolt
Wedges resumed despite the April 16 DB-timeout fix, and this time with
zero ESTABLISHED connections to Postgres — so not a DB deadlock. Most
suspicious piece is DbRetryMiddleware itself: its async path awaits
sync_to_async(...) inside an exception handler, a known-tricky pattern
on the asgiref thread pool. Django already handles stale-connection
recovery via conn_health_checks=True plus the psycopg connect_timeout/
statement_timeout added earlier, so the retry layer isn't load-bearing.

Also:
- RequestLoggingMiddleware is now async-capable, so the full chain
  stays async for async views instead of hopping through the thread
  pool on every request.
- Bump django-bolt 0.7.4 → 0.7.5 for any upstream fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:12:03 -04:00

71 lines
2.1 KiB
Python

import logging
import re
import time
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
logger = logging.getLogger('core.requests')
BOT_PATTERNS = re.compile(
r'bot|crawl|spider|slurp|semrush|ahrefs|bytespider|dotbot|mj12|gptbot|'
r'bingpreview|yandex|baiduspider|duckduckbot|facebookexternalhit|twitterbot|'
r'linkedinbot|applebot|google|mediapartners',
re.IGNORECASE,
)
SKIP_PATHS = ('/static/',)
SKIP_EXACT = {'/health', '/favicon.ico'}
def _detect_bot(user_agent: str) -> str | None:
"""Return the bot name from User-Agent, or None if not a bot."""
match = BOT_PATTERNS.search(user_agent)
return match.group(0) if match else None
def _log(request, response, duration_ms):
path = request.path
if path in SKIP_EXACT or path.startswith(SKIP_PATHS):
return
ua = request.META.get('HTTP_USER_AGENT', '')
bot = _detect_bot(ua)
if bot:
logger.info(
'[BOT:%s] %s %s %s %.0fms ua="%s"',
bot, request.method, path, response.status_code, duration_ms, ua[:200],
)
else:
logger.info(
'%s %s %s %.0fms',
request.method, path, response.status_code, duration_ms,
)
class RequestLoggingMiddleware:
"""Logs every request. Async-capable so django-bolt's ASGI chain stays
async end-to-end — avoids hopping through the asgiref thread pool on
every request, which piles up under load."""
sync_capable = True
async_capable = True
def __init__(self, get_response):
self.get_response = get_response
self.async_mode = iscoroutinefunction(get_response)
if self.async_mode:
markcoroutinefunction(self)
def __call__(self, request):
if self.async_mode:
return self._acall(request)
start = time.time()
response = self.get_response(request)
_log(request, response, (time.time() - start) * 1000)
return response
async def _acall(self, request):
start = time.time()
response = await self.get_response(request)
_log(request, response, (time.time() - start) * 1000)
return response