From 49283428c5ffe2131fe728f85c0e782502b34b1f Mon Sep 17 00:00:00 2001 From: mortezaei Date: Thu, 5 Feb 2026 12:08:34 +0330 Subject: [PATCH] Enhance logging in CourseOnlineClassTokenValidateAPIView and OnlineClassTokenManager for better traceability - Added detailed logging for GET and POST requests in CourseOnlineClassTokenValidateAPIView to track user actions and course validation. - Improved logging in OnlineClassTokenManager for token generation, storage, retrieval, and deletion processes, including error handling for Redis interactions. --- apps/course/views/course.py | 22 ++++++++++++------ utils/redis.py | 46 +++++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/apps/course/views/course.py b/apps/course/views/course.py index 7848f38..84d0a3e 100644 --- a/apps/course/views/course.py +++ b/apps/course/views/course.py @@ -452,11 +452,15 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): } ) def get(self, request, slug, *args, **kwargs): + logger.info(f"[Online Validate GET] Request received - slug={slug} user_id={request.user.id if request.user.is_authenticated else 'anonymous'}") + detail_view = CourseDetailAPIView() queryset = detail_view.get_queryset() course = get_object_or_404(queryset, slug=slug) user = request.user + logger.info(f"[Online Validate GET] Course found - course_id={course.id} slug={slug} is_online={course.is_online}") + # DEPRECATED: Polling approach replaced by webhook integration # Room status is now updated automatically via PlugNMeet webhooks # self._sync_room_status_with_plugnmeet(course) @@ -469,6 +473,8 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): user=user, ) + logger.info(f"[Online Validate GET] Success - user_id={user.id} course={slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") + return Response({ 'course': course_data, 'user': user_data, @@ -498,41 +504,43 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): } ) def post(self, request, *args, **kwargs): - logger.info(f"[Online Validate] Request received") + logger.info(f"[Online Validate POST] Request received - has_token={'token' in request.data}") serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) token_value = serializer.validated_data['token'] + logger.info(f"[Online Validate POST] Token extracted - token={token_value[:16]}...") + manager = OnlineClassTokenManager() try: payload = manager.get_payload(token_value) - logger.info(f"[Online Validate] Token decoded successfully") + logger.info(f"[Online Validate POST] Token decoded successfully - payload={payload}") except Exception as e: - logger.error(f"[Online Validate] Token decode failed - error={str(e)}") + logger.error(f"[Online Validate POST] Token decode failed - error={str(e)} type={type(e).__name__}") raise course_id = payload.get('course_id') user_id = payload.get('user_id') if not course_id or not user_id: - logger.warning(f"[Online Validate] Invalid token payload - course_id={course_id} user_id={user_id}") + logger.warning(f"[Online Validate POST] Invalid token payload - course_id={course_id} user_id={user_id}") raise AppAPIException({'message': 'Token payload is invalid.'}, status_code=status.HTTP_400_BAD_REQUEST) - logger.info(f"[Online Validate] Processing for user_id={user_id} course_id={course_id}") + logger.info(f"[Online Validate POST] Processing for user_id={user_id} course_id={course_id}") detail_view = CourseDetailAPIView() queryset = detail_view.get_queryset() course = get_object_or_404(queryset, pk=course_id) user = get_object_or_404(UserModel.objects.all(), pk=user_id) - logger.info(f"[Online Validate] Course found - slug={course.slug} is_online={course.is_online}") + logger.info(f"[Online Validate POST] Course found - slug={course.slug} is_online={course.is_online}") course_data = CourseDetailSerializer(course, context={'request': request}).data user_data = UserProfileSerializer(user, context={'request': request}).data metadata = self._build_metadata(course, payload, user=user) - logger.info(f"[Online Validate] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") + logger.info(f"[Online Validate POST] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") return Response({ 'course': course_data, diff --git a/utils/redis.py b/utils/redis.py index 545b330..b3f694d 100644 --- a/utils/redis.py +++ b/utils/redis.py @@ -2,6 +2,7 @@ import json import hashlib import random import secrets +import logging from datetime import datetime, timedelta from typing import Optional from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse @@ -13,6 +14,8 @@ from django.conf import settings from config.redis_config import RedisConfig from utils.exceptions import ServiceUnavailableException, NotFoundException +logger = logging.getLogger(__name__) + class RedisManager(RedisConfig): def __serialize(self, code, fullname, password): @@ -91,23 +94,52 @@ class OnlineClassTokenManager(RedisConfig): def generate_token(self, course_id: int, user_identifier: str) -> str: seed = f"{course_id}:{user_identifier}:{secrets.token_urlsafe(16)}" - return hashlib.sha256(seed.encode()).hexdigest() + token = hashlib.sha256(seed.encode()).hexdigest() + logger.info(f"[OnlineClassToken] Token generated - course_id={course_id} user={user_identifier} token={token[:16]}...") + return token def store_token(self, token: str, payload: dict, ttl: Optional[int] = None) -> None: data = { **payload, "generated_at": datetime.utcnow().isoformat() + "Z", } - self.redis.set(self._build_key(token), json.dumps(data), ex=ttl or self.ttl) + key = self._build_key(token) + ttl_value = ttl or self.ttl + logger.info(f"[OnlineClassToken] Storing token - key={key} ttl={ttl_value}s payload={payload}") + try: + self.redis.set(key, json.dumps(data), ex=ttl_value) + logger.info(f"[OnlineClassToken] Token stored successfully - key={key}") + except RedisError as e: + logger.error(f"[OnlineClassToken] Failed to store token - key={key} error={str(e)}") + raise def get_payload(self, token: str) -> dict: - stored = self.redis.get(self._build_key(token)) - if not stored: - raise NotFoundException("Token not found or has expired.") - return json.loads(stored) + key = self._build_key(token) + logger.info(f"[OnlineClassToken] Retrieving token - key={key} token={token[:16]}...") + try: + stored = self.redis.get(key) + if not stored: + logger.warning(f"[OnlineClassToken] Token not found or expired - key={key}") + raise NotFoundException("Token not found or has expired.") + payload = json.loads(stored) + logger.info(f"[OnlineClassToken] Token retrieved successfully - key={key} payload={payload}") + return payload + except RedisError as e: + logger.error(f"[OnlineClassToken] Redis error retrieving token - key={key} error={str(e)}") + raise + except json.JSONDecodeError as e: + logger.error(f"[OnlineClassToken] Invalid JSON in stored token - key={key} error={str(e)}") + raise NotFoundException("Invalid token data.") def delete_token(self, token: str) -> None: - self.redis.delete(self._build_key(token)) + key = self._build_key(token) + logger.info(f"[OnlineClassToken] Deleting token - key={key}") + try: + result = self.redis.delete(key) + logger.info(f"[OnlineClassToken] Token deleted - key={key} deleted={result}") + except RedisError as e: + logger.error(f"[OnlineClassToken] Failed to delete token - key={key} error={str(e)}") + raise @staticmethod def build_entry_url(token: str, base_url: Optional[str] = None) -> str: