diff --git a/backend/contributions/models.py b/backend/contributions/models.py index b9cf19c8..707794ea 100644 --- a/backend/contributions/models.py +++ b/backend/contributions/models.py @@ -9,6 +9,10 @@ import os import uuid +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('contributions') + class Category(BaseModel): """ @@ -224,7 +228,7 @@ def validate_multiplier_at_creation(sender, instance, **kwargs): float(instance.multiplier_at_creation) except (decimal.InvalidOperation, TypeError, ValueError): # If conversion fails, reset the multiplier to 1.0 - print(f"WARNING: Fixing corrupted multiplier_at_creation value for contribution {instance.id}") + logger.warning("Fixing corrupted multiplier_at_creation value") instance.multiplier_at_creation = 1.0 instance.frozen_global_points = instance.points diff --git a/backend/contributions/views.py b/backend/contributions/views.py index 17d48f68..7c6fa17f 100644 --- a/backend/contributions/views.py +++ b/backend/contributions/views.py @@ -117,16 +117,8 @@ def top_contributors(self, request, pk=None): contribution_count=Count('id') ).order_by('-total_points')[:10] - # Get user objects with select_related for efficiency - user_ids = [c['user'] for c in top_contributors] - users = { - user.id: user - for user in ContributionType.objects.get(pk=pk).contributions.filter( - user_id__in=user_ids - ).select_related('user', 'user__validator', 'user__builder').values_list('user', flat=True).distinct() - } - # Fetch users directly with optimization + user_ids = [c['user'] for c in top_contributors] from users.models import User users_dict = { user.id: user diff --git a/backend/deploy-apprunner-dev.sh b/backend/deploy-apprunner-dev.sh index a64ff4ac..7298cc29 100755 --- a/backend/deploy-apprunner-dev.sh +++ b/backend/deploy-apprunner-dev.sh @@ -72,7 +72,8 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU "GITHUB_ENCRYPTION_KEY": "$SSM_PREFIX/$SSM_ENV/github_encryption_key", "GITHUB_REPO_TO_STAR": "$SSM_PREFIX/$SSM_ENV/github_repo_to_star", "RECAPTCHA_PUBLIC_KEY": "$SSM_PREFIX/$SSM_ENV/recaptcha_public_key", - "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/$SSM_ENV/recaptcha_private_key" + "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/$SSM_ENV/recaptcha_private_key", + "CRON_SYNC_TOKEN": "$SSM_PREFIX/$SSM_ENV/cron_sync_token" }, "StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application" }, @@ -98,7 +99,7 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU } } EOF - + aws apprunner update-service \ --region $REGION \ --service-arn arn:aws:apprunner:$REGION:$ACCOUNT_ID:service/$SERVICE_NAME \ @@ -227,7 +228,8 @@ EOF "GITHUB_ENCRYPTION_KEY": "$SSM_PREFIX/$SSM_ENV/github_encryption_key", "GITHUB_REPO_TO_STAR": "$SSM_PREFIX/$SSM_ENV/github_repo_to_star", "RECAPTCHA_PUBLIC_KEY": "$SSM_PREFIX/$SSM_ENV/recaptcha_public_key", - "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/$SSM_ENV/recaptcha_private_key" + "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/$SSM_ENV/recaptcha_private_key", + "CRON_SYNC_TOKEN": "$SSM_PREFIX/$SSM_ENV/cron_sync_token" }, "StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application" }, @@ -253,7 +255,7 @@ EOF } } EOF - + aws apprunner create-service \ --region $REGION \ --cli-input-json file://apprunner-create-config.json diff --git a/backend/ethereum_auth/authentication.py b/backend/ethereum_auth/authentication.py index 834a270a..f5e04c39 100644 --- a/backend/ethereum_auth/authentication.py +++ b/backend/ethereum_auth/authentication.py @@ -3,7 +3,10 @@ from rest_framework import authentication, exceptions from rest_framework.authentication import SessionAuthentication +from tally.middleware.logging_utils import get_app_logger + User = get_user_model() +logger = get_app_logger('auth') class EthereumAuthentication(authentication.BaseAuthentication): @@ -11,27 +14,21 @@ class EthereumAuthentication(authentication.BaseAuthentication): Authentication class for Ethereum wallet addresses. Uses session authentication for the actual request validation. """ - + def authenticate(self, request): # Check if the session has an authenticated ethereum address ethereum_address = request.session.get('ethereum_address') authenticated = request.session.get('authenticated', False) - - # Debug logging - print(f"EthereumAuthentication - Session ID: {request.session.session_key}") - print(f"EthereumAuthentication - Ethereum Address: {ethereum_address}") - print(f"EthereumAuthentication - Authenticated: {authenticated}") - + if not ethereum_address or not authenticated: return None - + try: # Get user with the authenticated ethereum address user = User.objects.get(address__iexact=ethereum_address) - print(f"EthereumAuthentication - User found: {user.email}") return (user, None) except User.DoesNotExist: - print(f"EthereumAuthentication - User not found for address: {ethereum_address}") + logger.debug("User not found for authenticated session") return None diff --git a/backend/ethereum_auth/views.py b/backend/ethereum_auth/views.py index 646d3682..28de4227 100644 --- a/backend/ethereum_auth/views.py +++ b/backend/ethereum_auth/views.py @@ -13,8 +13,10 @@ from .models import Nonce from .authentication import CsrfExemptSessionAuthentication +from tally.middleware.logging_utils import get_app_logger User = get_user_model() +logger = get_app_logger('auth') def generate_nonce(length=32): @@ -137,10 +139,10 @@ def login(request): if referrer != user: user.referred_by = referrer user.save(update_fields=['referred_by']) - print(f"New user {ethereum_address} referred by {referrer.address}") + logger.debug("New user referred successfully") except User.DoesNotExist: # Invalid referral code, but don't fail the login - print(f"Invalid referral code provided during login: {referral_code}") + logger.warning("Invalid referral code provided during login") # Refresh user data from database to get referral_code from signal user.refresh_from_db() @@ -149,12 +151,9 @@ def login(request): request.session['ethereum_address'] = ethereum_address request.session['authenticated'] = True request.session.save() # Explicitly save the session - - # Debug logging - print(f"Login - Session ID: {request.session.session_key}") - print(f"Login - Setting ethereum_address: {ethereum_address}") - print(f"Login - Session data: {dict(request.session)}") - + + logger.debug("Login successful, session created") + # Return the authenticated user with referral data return Response({ 'authenticated': True, @@ -186,13 +185,7 @@ def verify_auth(request): """ ethereum_address = request.session.get('ethereum_address') authenticated = request.session.get('authenticated', False) - - # Debug logging - print(f"Verify - Session ID: {request.session.session_key}") - print(f"Verify - Ethereum Address: {ethereum_address}") - print(f"Verify - Authenticated: {authenticated}") - print(f"Verify - Session data: {dict(request.session)}") - + if authenticated and ethereum_address: try: user = User.objects.get(address__iexact=ethereum_address) diff --git a/backend/leaderboard/models.py b/backend/leaderboard/models.py index 9901a3be..a51eb18b 100644 --- a/backend/leaderboard/models.py +++ b/backend/leaderboard/models.py @@ -7,6 +7,9 @@ from django.db.models import Sum from utils.models import BaseModel from contributions.models import ContributionType, Contribution, Category +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('leaderboard') # Helper functions for leaderboard configuration @@ -370,8 +373,8 @@ def log_multiplier_creation(sender, instance, created, **kwargs): When a new multiplier is created, log it for debugging purposes. """ if created: - print(f"New global multiplier: {instance.contribution_type.name} - " - f"{instance.multiplier_value}x valid from {instance.valid_from.strftime('%Y-%m-%d %H:%M')}") + logger.debug(f"New global multiplier: {instance.contribution_type.name} - " + f"{instance.multiplier_value}x") @receiver(post_save, sender=Contribution) @@ -383,9 +386,8 @@ def update_leaderboard_on_contribution(sender, instance, created, **kwargs): # Only update if points have changed or it's a new contribution if created or kwargs.get('update_fields') is None or 'points' in kwargs.get('update_fields', []): # Log the contribution's point calculation - contribution_date_str = instance.contribution_date.strftime('%Y-%m-%d %H:%M') if instance.contribution_date else "N/A" - print(f"Contribution saved: {instance.points} points × {instance.multiplier_at_creation} = " - f"{instance.frozen_global_points} global points (contribution date: {contribution_date_str})") + logger.debug(f"Contribution saved: {instance.points} points × {instance.multiplier_at_creation} = " + f"{instance.frozen_global_points} global points") # Update the user's leaderboard entries update_user_leaderboard_entries(instance.user) diff --git a/backend/tally/middleware/__init__.py b/backend/tally/middleware/__init__.py new file mode 100644 index 00000000..802db835 --- /dev/null +++ b/backend/tally/middleware/__init__.py @@ -0,0 +1,5 @@ +"""Tally middleware package for logging.""" +from .api_logging import APILoggingMiddleware +from .db_logging import DBLoggingMiddleware + +__all__ = ['APILoggingMiddleware', 'DBLoggingMiddleware'] diff --git a/backend/tally/middleware/api_logging.py b/backend/tally/middleware/api_logging.py new file mode 100644 index 00000000..7434a99d --- /dev/null +++ b/backend/tally/middleware/api_logging.py @@ -0,0 +1,96 @@ +""" +API Layer Logging Middleware. + +Logs HTTP requests/responses for the [API] layer (Frontend <-> Backend). +""" +import time +from django.conf import settings + +from .logging_utils import ( + get_api_logger, + generate_correlation_id, + set_correlation_id, + clear_correlation_id, + format_bytes, +) + + +logger = get_api_logger() + + +class APILoggingMiddleware: + """ + Middleware to log HTTP requests and responses. + + DEBUG=true: Logs all requests with method, path, status, duration, size + DEBUG=false: Logs only errors (4xx, 5xx) + """ + + # Paths to skip logging + SKIP_PATHS = ( + '/static/', + '/media/', + '/health/', + '/favicon.ico', + '/__debug__/', + ) + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Skip logging for certain paths + if any(request.path.startswith(path) for path in self.SKIP_PATHS): + return self.get_response(request) + + # Generate and set correlation ID + correlation_id = generate_correlation_id() + set_correlation_id(correlation_id) + + # Start timing + start_time = time.time() + + # Process request + response = self.get_response(request) + + # Calculate duration + duration_ms = (time.time() - start_time) * 1000 + + # Get response size + response_size = len(response.content) if hasattr(response, 'content') else 0 + + # Get DB stats if available (set by DBLoggingMiddleware) + db_query_count = getattr(request, '_db_query_count', 0) + db_time_ms = getattr(request, '_db_time_ms', 0) + logic_time_ms = duration_ms - db_time_ms + + # Build log message with timing breakdown + if db_query_count > 0: + timing_info = f"{duration_ms:.0f}ms (db: {db_time_ms:.0f}ms/{db_query_count}q, logic: {logic_time_ms:.0f}ms)" + else: + timing_info = f"{duration_ms:.0f}ms" + + log_message = ( + f"{request.method} {request.path} " + f"{response.status_code} " + f"{timing_info} " + f"{format_bytes(response_size)}" + ) + + # Log based on status code and DEBUG setting + is_error = response.status_code >= 400 + + if is_error: + # Always log errors + if response.status_code >= 500: + logger.error(log_message) + else: + logger.warning(log_message) + elif settings.DEBUG: + # In debug mode, log all requests + logger.info(log_message) + + # Clear correlation ID + clear_correlation_id() + + return response diff --git a/backend/tally/middleware/db_logging.py b/backend/tally/middleware/db_logging.py new file mode 100644 index 00000000..ba0e9178 --- /dev/null +++ b/backend/tally/middleware/db_logging.py @@ -0,0 +1,53 @@ +""" +DB Layer Logging Middleware. + +Logs database query statistics for the [DB] layer (Backend <-> Database). +""" +from django.conf import settings +from django.db import connection, reset_queries + +from .logging_utils import get_db_logger + + +logger = get_db_logger() + + +class DBLoggingMiddleware: + """ + Middleware to log database query statistics per request. + + DEBUG=true: Logs query count and total time for each request + DEBUG=false: Only logs database errors + + Note: Query counting only works when DEBUG=True (Django limitation). + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Only do detailed logging in DEBUG mode (Django requirement) + if not settings.DEBUG: + return self.get_response(request) + + # Reset queries at start of request + reset_queries() + + # Process request + response = self.get_response(request) + + # Get query statistics + queries = connection.queries + query_count = len(queries) + + # Calculate total DB time (even if 0 queries) + total_time_ms = sum( + float(q.get('time', 0)) * 1000 + for q in queries + ) + + # Store stats on request for API middleware to use + request._db_query_count = query_count + request._db_time_ms = total_time_ms + + return response diff --git a/backend/tally/middleware/logging_utils.py b/backend/tally/middleware/logging_utils.py new file mode 100644 index 00000000..ffc99a5d --- /dev/null +++ b/backend/tally/middleware/logging_utils.py @@ -0,0 +1,137 @@ +""" +Logging utilities for the Tally application. + +Provides layer-specific loggers ([API], [DB], [APP]) and correlation ID tracking. +""" +import json +import logging +import threading +import uuid +from datetime import datetime +from typing import Optional + + +# Thread-local storage for correlation ID +_correlation_data = threading.local() + + +def set_correlation_id(correlation_id: str) -> None: + """Set the correlation ID for the current request.""" + _correlation_data.correlation_id = correlation_id + + +def get_correlation_id() -> Optional[str]: + """Get the correlation ID for the current request.""" + return getattr(_correlation_data, 'correlation_id', None) + + +def generate_correlation_id() -> str: + """Generate a new short correlation ID.""" + return uuid.uuid4().hex[:6] + + +def clear_correlation_id() -> None: + """Clear the correlation ID after request completes.""" + _correlation_data.correlation_id = None + + +def format_bytes(size_bytes: int) -> str: + """Format bytes into human-readable string.""" + if size_bytes < 1024: + return f"{size_bytes}B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f}KB" + else: + return f"{size_bytes / (1024 * 1024):.1f}MB" + + +class LayeredFormatter(logging.Formatter): + """ + Formatter that adds layer prefix to log messages. + + Output format: [LAYER] LEVEL: message (cid: xxx) + """ + + LAYER_MAP = { + 'tally.api': 'API', + 'tally.db': 'DB', + 'tally.app': 'APP', + } + + def format(self, record: logging.LogRecord) -> str: + # Determine layer from logger name + layer = 'APP' # default + for logger_name, layer_name in self.LAYER_MAP.items(): + if record.name.startswith(logger_name): + layer = layer_name + break + + # Get correlation ID if available + cid = get_correlation_id() + cid_suffix = f" (cid: {cid})" if cid else "" + + # Format the message + message = record.getMessage() + level = record.levelname + + return f"[{layer}] {level}: {message}{cid_suffix}" + + +class LayeredJSONFormatter(logging.Formatter): + """ + JSON formatter for production logging with layer identification. + """ + + LAYER_MAP = { + 'tally.api': 'API', + 'tally.db': 'DB', + 'tally.app': 'APP', + } + + def format(self, record: logging.LogRecord) -> str: + # Determine layer + layer = 'APP' + for logger_name, layer_name in self.LAYER_MAP.items(): + if record.name.startswith(logger_name): + layer = layer_name + break + + log_data = { + 'timestamp': datetime.utcnow().isoformat() + 'Z', + 'layer': layer, + 'level': record.levelname, + 'message': record.getMessage(), + } + + # Add correlation ID if available + cid = get_correlation_id() + if cid: + log_data['correlation_id'] = cid + + # Add exception info if present + if record.exc_info: + log_data['exception'] = self.formatException(record.exc_info) + + return json.dumps(log_data, default=str) + + +def get_api_logger() -> logging.Logger: + """Get the [API] layer logger.""" + return logging.getLogger('tally.api') + + +def get_db_logger() -> logging.Logger: + """Get the [DB] layer logger.""" + return logging.getLogger('tally.db') + + +def get_app_logger(name: Optional[str] = None) -> logging.Logger: + """ + Get the [APP] layer logger. + + Args: + name: Optional sub-logger name (e.g., 'auth', 'users') + """ + if name: + return logging.getLogger(f'tally.app.{name}') + return logging.getLogger('tally.app') diff --git a/backend/tally/settings.py b/backend/tally/settings.py index 9de4ac90..70e5773f 100644 --- a/backend/tally/settings.py +++ b/backend/tally/settings.py @@ -78,6 +78,7 @@ def get_required_env(key): ] MIDDLEWARE = [ + 'tally.middleware.APILoggingMiddleware', # Request logging (must be first for timing) 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -88,6 +89,7 @@ def get_required_env(key): 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'tally.middleware.DBLoggingMiddleware', # DB query logging (must be last) ] ROOT_URLCONF = 'tally.urls' @@ -269,7 +271,7 @@ def get_port_from_argv(): port = get_port_from_argv() SESSION_COOKIE_NAME = f'tally_sessionid_{port}' CSRF_COOKIE_NAME = f'tally_csrftoken_{port}' - print(f"[DEV] Using port-specific cookies for port {port}: {SESSION_COOKIE_NAME}") + # Port-specific cookies configured for development else: # Production settings SESSION_COOKIE_NAME = 'tally_sessionid' @@ -316,3 +318,67 @@ def get_port_from_argv(): # Cron job authentication token for validator sync endpoint CRON_SYNC_TOKEN = os.environ.get('CRON_SYNC_TOKEN', '') + +# ============================================================================= +# LOGGING CONFIGURATION +# ============================================================================= +# Layer-based logging: [API], [DB], [APP] +# DEBUG=true: All logs (requests, queries, app debug) +# DEBUG=false: Only errors and warnings + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'layered': { + '()': 'tally.middleware.logging_utils.LayeredFormatter', + }, + 'layered_json': { + '()': 'tally.middleware.logging_utils.LayeredJSONFormatter', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'layered' if DEBUG else 'layered_json', + }, + }, + 'loggers': { + # Application layer loggers + 'tally.api': { + 'handlers': ['console'], + 'level': 'DEBUG' if DEBUG else 'WARNING', + 'propagate': False, + }, + 'tally.db': { + 'handlers': ['console'], + 'level': 'DEBUG' if DEBUG else 'WARNING', + 'propagate': False, + }, + 'tally.app': { + 'handlers': ['console'], + 'level': 'DEBUG' if DEBUG else 'WARNING', + 'propagate': False, + }, + # Silence noisy Django loggers + 'django': { + 'handlers': ['console'], + 'level': 'WARNING', + 'propagate': False, + }, + 'django.request': { + 'handlers': ['console'], + 'level': 'ERROR', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console'], + 'level': 'WARNING', + 'propagate': False, + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'WARNING', + }, +} diff --git a/backend/users/cloudinary_service.py b/backend/users/cloudinary_service.py index a4f6157f..1faae0e7 100644 --- a/backend/users/cloudinary_service.py +++ b/backend/users/cloudinary_service.py @@ -2,10 +2,11 @@ import cloudinary.uploader from django.conf import settings from typing import Optional, Dict -import logging import time -logger = logging.getLogger(__name__) +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('cloudinary') class CloudinaryService: @@ -164,7 +165,7 @@ def delete_image(cls, public_id: str) -> bool: result = cloudinary.uploader.destroy(public_id) return result.get('result') == 'ok' except Exception as e: - logger.error(f"Failed to delete image {public_id}: {str(e)}") + logger.error(f"Failed to delete image: {str(e)}") return False @classmethod diff --git a/backend/users/genlayer_service.py b/backend/users/genlayer_service.py index 249aeac8..606e78f2 100644 --- a/backend/users/genlayer_service.py +++ b/backend/users/genlayer_service.py @@ -1,13 +1,14 @@ """ GenLayer blockchain integration service for checking user deployments. """ -import logging from typing import Dict, List, Optional, Any from django.conf import settings from genlayer_py import create_client from web3 import Web3 -logger = logging.getLogger(__name__) +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('genlayer') class GenLayerDeploymentService: @@ -27,7 +28,7 @@ def _initialize_client(self): # Use the built-in studionet chain configuration self.client = create_client(chain=studionet) - logger.info(f"GenLayer client initialized with StudioNet") + logger.debug("GenLayer client initialized with StudioNet") return True except Exception as e: logger.error(f"Failed to initialize GenLayer client: {str(e)}") @@ -66,11 +67,11 @@ def get_user_deployments(self, wallet_address: str) -> Dict[str, Any]: 'error': 'GenLayer client initialization failed' } - logger.info(f"Checking deployments for address: {wallet_address}") + logger.debug("Checking deployments") try: # Use the sim_getTransactionsForAddress method to get all transactions - logger.debug(f"Making request to Studio API for address: {wallet_address}") + logger.debug("Making request to Studio API") try: response = self.client.provider.make_request( @@ -79,22 +80,17 @@ def get_user_deployments(self, wallet_address: str) -> Dict[str, Any]: ) except Exception as api_error: # Log the raw error for debugging - logger.error(f"Raw API error for {wallet_address}: {str(api_error)}") + logger.error(f"Raw API error: {str(api_error)}") logger.error(f"Error type: {type(api_error).__name__}") - + # Try to get the raw response if available if hasattr(api_error, 'response'): logger.error(f"Response status code: {getattr(api_error.response, 'status_code', 'N/A')}") - logger.error(f"Response headers: {getattr(api_error.response, 'headers', 'N/A')}") - try: - logger.error(f"Response text: {api_error.response.text[:500]}") # First 500 chars - except: - logger.error("Could not get response text") - + # Try to handle common Studio API issues error_msg = str(api_error).lower() if "expecting value" in error_msg: - logger.warning(f"Studio API returned empty or non-JSON response for {wallet_address}") + logger.warning("Studio API returned empty or non-JSON response") # Return empty deployments but not an error - Studio might not have data return { 'has_deployments': False, @@ -107,7 +103,7 @@ def get_user_deployments(self, wallet_address: str) -> Dict[str, Any]: # Extract transactions from the response transactions = response.get('result', []) - logger.info(f"Found {len(transactions)} transactions for {wallet_address}") + logger.debug(f"Found {len(transactions)} transactions") # Check for contract deployments deployments = [] @@ -144,12 +140,12 @@ def get_user_deployments(self, wallet_address: str) -> Dict[str, Any]: 'type': tx_type } deployments.append(deployment_info) - logger.info(f"Found deployment: {tx_hash}") + logger.debug("Found deployment") - logger.info(f"Address {wallet_address} has {len(deployments)} deployments") + logger.debug(f"Found {len(deployments)} deployments") except Exception as e: - logger.error(f"Error checking deployments via sim_getTransactionsForAddress: {str(e)}") + logger.error(f"Error checking deployments via API: {str(e)}") deployments = [] return { @@ -160,7 +156,7 @@ def get_user_deployments(self, wallet_address: str) -> Dict[str, Any]: } except Exception as e: - logger.error(f"Error checking deployments for address {wallet_address}: {str(e)}") + logger.error(f"Error checking deployments: {str(e)}") return { 'has_deployments': False, 'deployments': [], @@ -253,7 +249,7 @@ def get_contract_details(self, contract_address: str) -> Dict[str, Any]: } except Exception as e: - logger.error(f"Error getting contract details for {contract_address}: {str(e)}") + logger.error(f"Error getting contract details: {str(e)}") return { 'address': contract_address, 'status': 'unknown', diff --git a/backend/users/github_oauth.py b/backend/users/github_oauth.py index a7cf2bd0..89c052e7 100644 --- a/backend/users/github_oauth.py +++ b/backend/users/github_oauth.py @@ -2,7 +2,6 @@ GitHub OAuth authentication handling """ import secrets -import logging from urllib.parse import urlencode import requests @@ -18,8 +17,9 @@ from cryptography.fernet import Fernet, InvalidToken from .models import User +from tally.middleware.logging_utils import get_app_logger -logger = logging.getLogger(__name__) +logger = get_app_logger('github_oauth') # Cache to track used OAuth codes (prevents duplicate exchanges) # Format: {code: timestamp} @@ -58,7 +58,7 @@ def github_oauth_initiate(request): """Initiate GitHub OAuth flow""" # User is guaranteed to be authenticated due to @permission_classes([IsAuthenticated]) user_id = request.user.id - logger.info(f"GitHub OAuth initiated by authenticated user {user_id}") + logger.debug("GitHub OAuth initiated") # Generate state token with user ID embedded state_data = { @@ -69,7 +69,7 @@ def github_oauth_initiate(request): # Sign the state data to make it tamper-proof and not rely on session # This works even if session cookies don't persist across OAuth redirects state = signing.dumps(state_data, salt='github_oauth_state') - logger.info(f"Generated signed OAuth state for user {user_id}") + logger.debug("Generated signed OAuth state") # Build GitHub OAuth URL with minimal read-only permissions # Empty scope gives read access to public user info including starred repos @@ -95,7 +95,7 @@ def github_oauth_callback(request): error = request.GET.get('error') error_description = request.GET.get('error_description') - logger.info(f"OAuth callback - Session key: {request.session.session_key}") + logger.debug("OAuth callback received") # Handle errors from GitHub if error: @@ -122,7 +122,7 @@ def github_oauth_callback(request): # Unsign the state with max_age of 10 minutes to prevent replay attacks state_data = signing.loads(state, salt='github_oauth_state', max_age=600) user_id = state_data.get('user_id') - logger.info(f"Successfully validated signed OAuth state for user_id: {user_id}") + logger.debug("Successfully validated signed OAuth state") except signing.SignatureExpired: logger.error("OAuth state token has expired (>10 minutes old)") return render(request, 'github_callback.html', { @@ -147,11 +147,11 @@ def github_oauth_callback(request): del _used_oauth_codes[expired_code] if expired_codes: - logger.info(f"Cleaned up {len(expired_codes)} expired OAuth codes") + logger.debug(f"Cleaned up {len(expired_codes)} expired OAuth codes") # Check if this code has already been used (prevent duplicate exchanges) if code in _used_oauth_codes: - logger.warning(f"OAuth code {code[:10]}... has already been used, rejecting duplicate request") + logger.warning("OAuth code already used, rejecting duplicate request") return render(request, 'github_callback.html', { 'success': False, 'error': 'code_already_used', @@ -161,10 +161,10 @@ def github_oauth_callback(request): # Mark code as used immediately (before exchange attempt) _used_oauth_codes[code] = timezone.now() - logger.info(f"Marked OAuth code {code[:10]}... as used ({len(_used_oauth_codes)} codes in cache)") + logger.debug(f"Marked OAuth code as used ({len(_used_oauth_codes)} codes in cache)") # Exchange code for access token - logger.info(f"Attempting to exchange OAuth code {code[:10]}... for access token") + logger.debug("Attempting to exchange OAuth code for access token") token_url = "https://github.com/login/oauth/access_token" token_params = { 'client_id': settings.GITHUB_CLIENT_ID, @@ -181,7 +181,7 @@ def github_oauth_callback(request): token_data = token_response.json() if 'error' in token_data: - logger.error(f"GitHub token exchange error: {token_data}") + logger.error("GitHub token exchange error") # Check if it's because the code was already used if token_data.get('error') == 'bad_verification_code': return render(request, 'github_callback.html', { @@ -231,9 +231,9 @@ def github_oauth_callback(request): try: user = User.objects.get(id=user_id) - logger.info(f"Found user {user.id} from state token") + logger.debug("Found user from state token") except User.DoesNotExist: - logger.error(f"User {user_id} from state not found") + logger.error("User from state not found") return render(request, 'github_callback.html', { 'success': False, 'error': 'user_not_found', @@ -247,7 +247,7 @@ def github_oauth_callback(request): ).exclude(id=user.id).first() if existing_user: - logger.warning(f"GitHub account already linked to another user") + logger.warning("GitHub account already linked to another user") return render(request, 'github_callback.html', { 'success': False, 'error': 'already_linked', @@ -262,7 +262,7 @@ def github_oauth_callback(request): user.github_linked_at = timezone.now() user.save() - logger.info(f"GitHub account linked for user {user.id}") + logger.debug("GitHub account linked successfully") # Success! Render the callback template return render(request, 'github_callback.html', { 'success': True, @@ -297,7 +297,7 @@ def disconnect_github(request): 'message': 'GitHub account disconnected successfully' }, status=status.HTTP_200_OK) except Exception as e: - logger.error(f"Failed to disconnect GitHub for user {request.user.id}: {e}") + logger.error(f"Failed to disconnect GitHub: {e}") return Response({ 'error': 'Failed to disconnect GitHub account' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -326,7 +326,7 @@ def check_repo_star(request): except InvalidToken: # Token decryption failed, likely due to encryption key change # Clear the invalid token - logger.warning(f"Failed to decrypt GitHub token for user id={user.id}, clearing token") + logger.warning("Failed to decrypt GitHub token, clearing token") user.github_access_token = '' user.save() # Continue without auth header to use public API @@ -367,7 +367,7 @@ def check_repo_star(request): }, status=status.HTTP_200_OK) except requests.RequestException as e: - logger.error(f"Failed to check star status for {user.github_username}: {e}") + logger.error(f"Failed to check star status: {e}") return Response({ 'has_starred': False, 'repo': settings.GITHUB_REPO_TO_STAR, diff --git a/backend/users/signals.py b/backend/users/signals.py index 2bbc0441..300d897c 100644 --- a/backend/users/signals.py +++ b/backend/users/signals.py @@ -2,8 +2,12 @@ import secrets from django.db.models.signals import post_save from django.dispatch import receiver + +from tally.middleware.logging_utils import get_app_logger from .models import User +logger = get_app_logger('users') + def generate_unique_referral_code(): """ @@ -12,12 +16,12 @@ def generate_unique_referral_code(): """ characters = string.ascii_uppercase + string.digits # A-Z, 0-9 max_attempts = 100 - + for _ in range(max_attempts): code = ''.join(secrets.choice(characters) for _ in range(8)) if not User.objects.filter(referral_code=code).exists(): return code - + # If we can't find a unique code after max_attempts, raise an error raise ValueError("Unable to generate unique referral code after maximum attempts") @@ -34,4 +38,4 @@ def create_referral_code(sender, instance, created, **kwargs): User.objects.filter(pk=instance.pk).update(referral_code=referral_code) except Exception as e: # Log the error but don't fail user creation - print(f"Failed to generate referral code for user {instance.id}: {str(e)}") \ No newline at end of file + logger.error(f"Failed to generate referral code: {str(e)}") \ No newline at end of file diff --git a/backend/users/views.py b/backend/users/views.py index 4f9bb07a..36757fe5 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -13,11 +13,12 @@ from contributions.models import Contribution from leaderboard.models import LeaderboardEntry from web3 import Web3 -import logging import secrets import string -logger = logging.getLogger(__name__) +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('users') class UserViewSet(viewsets.ReadOnlyModelViewSet): @@ -192,7 +193,7 @@ def upload_profile_image(self, request): }) except Exception as e: - logger.error(f"Profile image upload failed for user {request.user.id}: {str(e)}") + logger.error(f"Profile image upload failed: {str(e)}") return Response( {'error': 'Failed to upload image'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -249,7 +250,7 @@ def upload_banner_image(self, request): }) except Exception as e: - logger.error(f"Banner image upload failed for user {request.user.id}: {str(e)}") + logger.error(f"Banner image upload failed: {str(e)}") return Response( {'error': 'Failed to upload image'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -380,7 +381,7 @@ def start_builder_journey(self, request): }, status=status.HTTP_201_CREATED) except Exception as e: - logger.error(f"Failed to start builder journey for user {user.id}: {str(e)}") + logger.error(f"Failed to start builder journey: {str(e)}") return Response( {'error': f'Failed to start journey: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -512,7 +513,7 @@ def complete_builder_journey(self, request): status=status.HTTP_400_BAD_REQUEST ) except Exception as e: - logger.warning(f"Failed to check balance for {user.address}: {str(e)}") + logger.warning(f"Failed to check balance: {str(e)}") # If we can't check balance, we'll allow proceeding (fail open) pass @@ -531,10 +532,10 @@ def complete_builder_journey(self, request): status=status.HTTP_400_BAD_REQUEST ) - logger.info(f"User {user.id} has {deployment_result.get('deployment_count', 0)} deployments") + logger.debug(f"Deployment check passed: {deployment_result.get('deployment_count', 0)} deployments") except Exception as e: - logger.error(f"Failed to check deployments for {user.address}: {str(e)}") + logger.error(f"Failed to check deployments: {str(e)}") return Response( {'error': 'Failed to verify contract deployments. Please try again later.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -579,7 +580,7 @@ def complete_builder_journey(self, request): except Exception as e: # Transaction will be rolled back automatically - logger.error(f"Failed to complete builder journey for user {user.id}: {str(e)}") + logger.error(f"Failed to complete builder journey: {str(e)}") return Response( {'error': f'Failed to complete journey: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -640,14 +641,12 @@ def check_deployments(self, request): # Check for deployments deployment_result = genlayer_service.get_user_deployments(checksum_address) - # Log the check for monitoring - logger.info(f"Deployment check for user {user.id} (address: {user.address}): " - f"{deployment_result.get('deployment_count', 0)} deployments found") + logger.debug(f"Deployment check: {deployment_result.get('deployment_count', 0)} deployments found") return Response(deployment_result, status=status.HTTP_200_OK) except Exception as e: - logger.error(f"Error checking deployments for user {user.id}: {str(e)}") + logger.error(f"Error checking deployments: {str(e)}") return Response({ 'has_deployments': False, 'deployments': [], @@ -684,7 +683,7 @@ def deployment_status(self, request): }) except Exception as e: - logger.error(f"Error checking deployment status for user {user.id}: {str(e)}") + logger.error(f"Error checking deployment status: {str(e)}") return Response({ 'has_deployments': False, 'deployment_count': 0, diff --git a/backend/utils/pagination.py b/backend/utils/pagination.py index 6fcc6e8c..175c37a2 100644 --- a/backend/utils/pagination.py +++ b/backend/utils/pagination.py @@ -3,6 +3,10 @@ import decimal from django.db import connection +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('utils') + class SafePageNumberPagination(PageNumberPagination): """ @@ -24,7 +28,7 @@ def paginate_queryset(self, queryset, request, view=None): return super().paginate_queryset(queryset, request, view) except decimal.InvalidOperation: # If we encounter decimal error, we need to handle it directly in the database - print("WARNING: Encountered invalid decimal error in pagination. Applying direct database fix...") + logger.warning("Encountered invalid decimal error in pagination. Applying direct database fix...") # Emergency fix - run direct SQL to fix the corrupted data with connection.cursor() as cursor: diff --git a/backend/validators/genlayer_validators_service.py b/backend/validators/genlayer_validators_service.py index 4ac8b887..c7f0de2b 100644 --- a/backend/validators/genlayer_validators_service.py +++ b/backend/validators/genlayer_validators_service.py @@ -2,12 +2,13 @@ GenLayer blockchain integration service for validator wallet synchronization. Handles RPC calls to Staking, Factory, and ValidatorWallet contracts. """ -import logging from typing import Dict, List, Optional, Any from django.conf import settings from web3 import Web3 -logger = logging.getLogger(__name__) +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('validators') # Contract ABIs @@ -240,7 +241,7 @@ def fetch_validator_view(self, validator_address: str) -> Optional[Dict[str, Any 'live': view[11] } except Exception as e: - logger.error(f"Error fetching validator view for {validator_address}: {str(e)}") + logger.error(f"Error fetching validator view: {str(e)}") return None def fetch_operator_for_wallet(self, wallet_address: str) -> Optional[str]: @@ -264,7 +265,7 @@ def fetch_operator_for_wallet(self, wallet_address: str) -> Optional[str]: return operator except Exception as e: - logger.error(f"Error fetching operator for wallet {wallet_address}: {str(e)}") + logger.error(f"Error fetching operator: {str(e)}") return None def fetch_validator_identity(self, wallet_address: str) -> Optional[Dict[str, Any]]: @@ -292,7 +293,7 @@ def fetch_validator_identity(self, wallet_address: str) -> Optional[Dict[str, An 'github': identity[7], } except Exception as e: - logger.error(f"Error fetching identity for wallet {wallet_address}: {str(e)}") + logger.error(f"Error fetching identity: {str(e)}") return None def sync_all_validators(self) -> Dict[str, Any]: @@ -357,7 +358,7 @@ def sync_all_validators(self) -> Dict[str, Any]: stats=stats ) except Exception as e: - logger.error(f"Error processing validator {address}: {str(e)}") + logger.error(f"Error processing validator: {str(e)}") stats['errors'] += 1 except Exception as e: diff --git a/backend/validators/node_version.py b/backend/validators/node_version.py index f6c7b439..169f790b 100644 --- a/backend/validators/node_version.py +++ b/backend/validators/node_version.py @@ -7,6 +7,10 @@ from packaging import version import re +from tally.middleware.logging_utils import get_app_logger + +logger = get_app_logger('validators') + class NodeVersionMixin(models.Model): """ @@ -52,7 +56,7 @@ def version_matches_or_higher(self, target_version): return current >= target except Exception as e: # If parsing fails, fall back to string comparison - print(f"Version parsing error: {e}") + logger.warning(f"Version parsing error: {e}") return self.node_version >= target_version def save(self, *args, **kwargs): diff --git a/frontend/src/routes/AllContributions.svelte b/frontend/src/routes/AllContributions.svelte index 3237fd9a..5e72e580 100644 --- a/frontend/src/routes/AllContributions.svelte +++ b/frontend/src/routes/AllContributions.svelte @@ -215,6 +215,7 @@ bind:selectedContributionType bind:selectedMission defaultContributionType={appliedTypeId ? Number(appliedTypeId) : null} + defaultMission={appliedMissionId ? Number(appliedMissionId) : null} onlySubmittable={false} /> diff --git a/frontend/src/routes/ContributionTypeDetail.svelte b/frontend/src/routes/ContributionTypeDetail.svelte index 882d1b71..daebdf22 100644 --- a/frontend/src/routes/ContributionTypeDetail.svelte +++ b/frontend/src/routes/ContributionTypeDetail.svelte @@ -509,14 +509,15 @@ rank: i + 1, user_details: { name: c.name, - address: c.address + address: c.address, + profile_image_url: c.profile_image_url }, total_points: c.total_points }))} loading={false} error={null} showHeader={false} - compact={true} + compact={false} hideAddress={true} /> diff --git a/package-lock.json b/package-lock.json index c72f6782..43462773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "berlin", + "name": "lisbon", "lockfileVersion": 3, "requires": true, "packages": {