""" میان‌افزارِ سبکِ لاگِ درخواست‌های API — سطحِ پروژه. ⚠️ افزوده شده: پروژه چند logger پراکنده‌ی اپلیکیشنی داشت (accounts.services، ai_tutor.views، ...) اما هیچ لاگِ یکپارچه‌ای در سطحِ پروژه برای *همه‌ی* درخواست‌های API وجود نداشت؛ یعنی درخواست‌های عادی، ناموفق، یا الگوهای مشکوک (مثلاً یک IP/کاربر که پشتِ سرِ هم درخواستِ ناموفق می‌فرسته) به‌صورتِ یکدست قابلِ ردیابی نبودن. این میان‌افزار فقط یک خط لاگِ خلاصه به‌ازای هر درخواستِ زیرِ /api/ ثبت می‌کنه: متد، مسیر، کدِ وضعیتِ پاسخ، مدت‌زمان، و شناسه‌ی کاربرِ احرازهویت‌شده (اگه وجود داشته باشه). نکاتِ امنیتی/حریمِ خصوصی: - هیچ‌وقت بدنه‌ی درخواست/پاسخ، query string، هدرها (از جمله Authorization، Idempotency-Key و غیره)، رمزعبور/OTP، access/refresh token، کلیدِ API، یا سکرتِ webhook لاگ نمی‌شه — فقط request.path (بدونِ query string) و متادیتای بی‌ضررِ بالا. - در صورتِ هر خطای غیرمنتظره در خودِ فرآیندِ لاگ‌کردن (مثلاً یک مقدارِ غیرمنتظره برای user)، این میان‌افزار هیچ‌وقت نباید جریانِ درخواست/پاسخ را بشکنه؛ به همین خاطر کل بخشِ لاگ داخلِ try/except قرار گرفته. - ⚠️ اصلاح: قبلاً اگه self.get_response(request) خودش یک exceptionِ دست‌نخورده (unhandled) بالا می‌آورد، اون exception قبل از رسیدن به بلوکِ لاگ از __call__ خارج می‌شد؛ یعنی خطاهای ۵۰۰ اصلاً لاگ نمی‌شدن. حالا self.get_response در try/except قرار گرفته: در صورتِ exception، یک خطِ لاگِ سطحِ ERROR (با متد/مسیر/مدت/user_id، بدونِ جزئیاتِ حساس) ثبت می‌شه و بعد همون exceptionِ اصلی بدونِ تغییر دوباره raise می‌شه (تا رفتارِ مدیریتِ خطای Django/DRF دست‌نخورده بمونه). - ⚠️ اصلاحِ این دور: پیامِ لاگِ مسیرِ exception از یک placeholderِ واقعیِ status=%s استفاده می‌کنه (هم‌سو با مسیرِ موفق)، و status=500 حالا از طریقِ آرگومانِ status به _log_safely() صریحاً پاس داده می‌شه، نه به‌صورتِ متنِ ثابت داخلِ رشته. این هم پیام رو با مسیرِ موفق یکدست می‌کنه و هم از ناهماهنگیِ تعدادِ آرگومان‌های % formatting جلوگیری می‌کنه. """ import logging import time logger = logging.getLogger(__name__) # فقط مسیرهای API لاگ می‌شن؛ درخواست‌های استاتیک/غیرِ API (که این پروژه # اصلاً مسیرِ URL جداگانه‌ای براشون در config/urls.py تعریف نکرده) عمداً # نادیده گرفته می‌شن تا لاگ شلوغ/بی‌فایده تولید نشه. _LOGGED_PATH_PREFIX = '/api/' def _user_id_for_log(request): """شناسه‌ی کاربرِ احرازهویت‌شده برای لاگ، یا None (کاربرِ ناشناس/بدونِ user).""" user = getattr(request, 'user', None) return user.id if getattr(user, 'is_authenticated', False) else None class APIRequestLoggingMiddleware: """میان‌افزارِ Django برای لاگِ خلاصه‌ی هر درخواستِ API (متد/مسیر/وضعیت/مدت/کاربر).""" def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if not request.path.startswith(_LOGGED_PATH_PREFIX): return self.get_response(request) start = time.monotonic() try: response = self.get_response(request) except Exception: self._log_safely( logger.error, 'api_request method=%s path=%s status=%s duration_ms=%.1f user_id=%s (unhandled exception)', request, start, status=500, ) raise self._log_safely( logger.info, 'api_request method=%s path=%s status=%s duration_ms=%.1f user_id=%s', request, start, response.status_code, ) return response def _log_safely(self, log_func, message, request, start, status=None): # لاگ‌کردن هیچ‌وقت نباید خودش دلیلِ شکستِ یک درخواست بشه (چه در # مسیرِ موفق، چه در مسیرِ exception)؛ به همین خاطر کل بخشِ لاگ # داخلِ try/except قرار گرفته. try: duration_ms = (time.monotonic() - start) * 1000 if status is None: log_func(message, request.method, request.path, duration_ms, _user_id_for_log(request)) else: log_func(message, request.method, request.path, status, duration_ms, _user_id_for_log(request)) except Exception: logger.exception('api_request logging failed')