diff --git a/app/bot.py b/app/bot.py
index 369fccc8..dd657a2b 100644
--- a/app/bot.py
+++ b/app/bot.py
@@ -1,6 +1,5 @@
-import logging
-
import redis.asyncio as redis
+import structlog
from aiogram import Bot, Dispatcher, types
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.storage.redis import RedisStorage
@@ -63,6 +62,7 @@ from app.handlers.stars_payments import register_stars_handlers
from app.middlewares.auth import AuthMiddleware
from app.middlewares.blacklist import BlacklistMiddleware
from app.middlewares.button_stats import ButtonStatsMiddleware
+from app.middlewares.context_binding import ContextVarsMiddleware
from app.middlewares.global_error import GlobalErrorMiddleware
from app.middlewares.logging import LoggingMiddleware
from app.middlewares.maintenance import MaintenanceMiddleware
@@ -75,14 +75,14 @@ from app.utils.message_patch import patch_message_methods
patch_message_methods()
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def debug_callback_handler(callback: types.CallbackQuery):
logger.info('🔍 DEBUG CALLBACK:')
- logger.info(f' - Data: {callback.data}')
- logger.info(f' - User: {callback.from_user.id}')
- logger.info(f' - Username: {callback.from_user.username}')
+ logger.info('Data', callback_data=callback.data)
+ logger.info('User', from_user_id=callback.from_user.id)
+ logger.info('Username', username=callback.from_user.username)
async def setup_bot() -> tuple[Bot, Dispatcher]:
@@ -90,7 +90,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
await cache.connect()
logger.info('Кеш инициализирован')
except Exception as e:
- logger.warning(f'Кеш не инициализирован: {e}')
+ logger.warning('Кеш не инициализирован', error=e)
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
@@ -106,12 +106,15 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
storage = RedisStorage(redis_client)
logger.info('Подключено к Redis для FSM storage')
except Exception as e:
- logger.warning(f'Не удалось подключиться к Redis: {e}')
+ logger.warning('Не удалось подключиться к Redis', error=e)
logger.info('Используется MemoryStorage для FSM')
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
+ dp.message.middleware(ContextVarsMiddleware())
+ dp.callback_query.middleware(ContextVarsMiddleware())
+ dp.pre_checkout_query.middleware(ContextVarsMiddleware())
dp.message.middleware(GlobalErrorMiddleware())
dp.callback_query.middleware(GlobalErrorMiddleware())
dp.pre_checkout_query.middleware(GlobalErrorMiddleware())
@@ -205,7 +208,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
await maintenance_service.start_monitoring()
logger.info('Мониторинг техработ запущен')
except Exception as e:
- logger.error(f'Ошибка запуска мониторинга техработ: {e}')
+ logger.error('Ошибка запуска мониторинга техработ', error=e)
else:
logger.info('Мониторинг техработ отключен настройками')
@@ -236,7 +239,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
'Установите MINIAPP_CUSTOM_URL.'
)
elif settings.is_cabinet_mode():
- logger.info(f'🏠 Режим Cabinet активен, базовый URL: {settings.MINIAPP_CUSTOM_URL}')
+ logger.info('🏠 Режим Cabinet активен, базовый URL', MINIAPP_CUSTOM_URL=settings.MINIAPP_CUSTOM_URL)
# Load per-section button styles cache
if settings.is_cabinet_mode():
@@ -245,7 +248,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
await load_button_styles_cache()
except Exception as e:
- logger.warning(f'Failed to load button styles cache: {e}')
+ logger.warning('Failed to load button styles cache', error=e)
logger.info('Бот успешно настроен')
@@ -257,10 +260,10 @@ async def shutdown_bot():
await maintenance_service.stop_monitoring()
logger.info('Мониторинг техработ остановлен')
except Exception as e:
- logger.error(f'Ошибка остановки мониторинга: {e}')
+ logger.error('Ошибка остановки мониторинга', error=e)
try:
await cache.close()
logger.info('Соединения с кешем закрыты')
except Exception as e:
- logger.error(f'Ошибка закрытия кеша: {e}')
+ logger.error('Ошибка закрытия кеша', error=e)
diff --git a/app/cabinet/auth/oauth_providers.py b/app/cabinet/auth/oauth_providers.py
index e83cab85..0a22a881 100644
--- a/app/cabinet/auth/oauth_providers.py
+++ b/app/cabinet/auth/oauth_providers.py
@@ -1,18 +1,18 @@
"""OAuth 2.0 provider implementations for cabinet authentication."""
-import logging
import secrets
from abc import ABC, abstractmethod
from typing import Any, TypedDict
import httpx
+import structlog
from pydantic import BaseModel
from app.config import settings
from app.utils.cache import cache, cache_key
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
STATE_TTL_SECONDS = 600 # 10 minutes
diff --git a/app/cabinet/dependencies.py b/app/cabinet/dependencies.py
index 73bdcc08..e10c76bd 100644
--- a/app/cabinet/dependencies.py
+++ b/app/cabinet/dependencies.py
@@ -1,8 +1,8 @@
"""FastAPI dependencies for cabinet module."""
import asyncio
-import logging
+import structlog
from aiogram import Bot
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -18,7 +18,7 @@ from app.services.maintenance_service import maintenance_service
from .auth.jwt_handler import get_token_payload
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
security = HTTPBearer(auto_error=False)
@@ -161,10 +161,12 @@ async def get_current_cabinet_user(
except HTTPException:
raise
except TimeoutError:
- logger.warning(f'Timeout checking channel subscription for user {user.telegram_id}')
+ logger.warning('Timeout checking channel subscription for user', telegram_id=user.telegram_id)
# Don't block user if check times out
except Exception as e:
- logger.warning(f'Failed to check channel subscription for user {user.telegram_id}: {e}')
+ logger.warning(
+ 'Failed to check channel subscription for user', telegram_id=user.telegram_id, error=e
+ )
# Don't block user if check fails
return user
diff --git a/app/cabinet/routes/admin_apps.py b/app/cabinet/routes/admin_apps.py
index 10169673..b3b1a7ec 100644
--- a/app/cabinet/routes/admin_apps.py
+++ b/app/cabinet/routes/admin_apps.py
@@ -1,9 +1,9 @@
"""Admin routes for managing VPN applications in app-config.json."""
import json
-import logging
from pathlib import Path
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +16,7 @@ from app.services.system_settings_service import bot_configuration_service
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/apps', tags=['Cabinet Admin Apps'])
@@ -231,7 +231,7 @@ async def create_app(
config['platforms'] = platforms
_save_config(config)
- logger.info(f"Admin {admin.id} created app '{request.app.id}' for platform '{platform}'")
+ logger.info('Admin created app for platform', admin_id=admin.id, app_id=request.app.id, platform=platform)
return request.app
@@ -274,7 +274,7 @@ async def update_app(
config['platforms'] = platforms
_save_config(config)
- logger.info(f"Admin {admin.id} updated app '{app_id}' in platform '{platform}'")
+ logger.info('Admin updated app in platform', admin_id=admin.id, app_id=app_id, platform=platform)
return request.app
@@ -310,7 +310,7 @@ async def delete_app(
config['platforms'] = platforms
_save_config(config)
- logger.info(f"Admin {admin.id} deleted app '{app_id}' from platform '{platform}'")
+ logger.info('Admin deleted app from platform', admin_id=admin.id, app_id=app_id, platform=platform)
return {'status': 'deleted', 'app_id': app_id}
@@ -355,7 +355,7 @@ async def reorder_apps(
config['platforms'] = platforms
_save_config(config)
- logger.info(f"Admin {admin.id} reordered apps in platform '{platform}'")
+ logger.info('Admin reordered apps in platform', admin_id=admin.id, platform=platform)
return {'status': 'reordered', 'order': request.app_ids}
@@ -374,7 +374,7 @@ async def update_branding(
config['config']['branding'] = request.branding.model_dump()
_save_config(config)
- logger.info(f'Admin {admin.id} updated branding')
+ logger.info('Admin updated branding', admin_id=admin.id)
return request.branding
@@ -434,7 +434,14 @@ async def copy_app_to_platform(
config['platforms'] = platforms
_save_config(config)
- logger.info(f"Admin {admin.id} copied app '{app_id}' from '{platform}' to '{target_platform}' as '{new_id}'")
+ logger.info(
+ 'Admin copied app from to as',
+ admin_id=admin.id,
+ app_id=app_id,
+ platform=platform,
+ target_platform=target_platform,
+ new_id=new_id,
+ )
return {'status': 'copied', 'new_id': new_id, 'target_platform': target_platform}
@@ -498,9 +505,9 @@ async def set_remnawave_config_uuid(
try:
await bot_configuration_service.set_value(db, 'CABINET_REMNA_SUB_CONFIG', uuid_value)
await db.commit()
- logger.info(f"Admin {admin.id} updated CABINET_REMNA_SUB_CONFIG to '{uuid_value}'")
+ logger.info('Admin updated CABINET_REMNA_SUB_CONFIG to', admin_id=admin.id, uuid_value=uuid_value)
except Exception as e:
- logger.error(f'Error saving RemnaWave config UUID: {e}')
+ logger.error('Error saving RemnaWave config UUID', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to save configuration',
@@ -547,7 +554,7 @@ async def get_remnawave_subscription_config(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Error fetching RemnaWave config: {e}')
+ logger.error('Error fetching RemnaWave config', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Failed to fetch config from RemnaWave: {e!s}',
@@ -572,7 +579,7 @@ async def list_remnawave_subscription_configs(
for c in configs
]
except Exception as e:
- logger.error(f'Error listing RemnaWave configs: {e}')
+ logger.error('Error listing RemnaWave configs', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Failed to fetch configs from RemnaWave: {e!s}',
diff --git a/app/cabinet/routes/admin_ban_system.py b/app/cabinet/routes/admin_ban_system.py
index 16c57166..000183f8 100644
--- a/app/cabinet/routes/admin_ban_system.py
+++ b/app/cabinet/routes/admin_ban_system.py
@@ -1,8 +1,8 @@
"""Admin routes for Ban System monitoring in cabinet."""
-import logging
from typing import Any
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from app.config import settings
@@ -45,7 +45,7 @@ from ..schemas.ban_system import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/ban-system', tags=['Cabinet Admin Ban System'])
@@ -53,9 +53,11 @@ router = APIRouter(prefix='/admin/ban-system', tags=['Cabinet Admin Ban System']
def _get_ban_api() -> BanSystemAPI:
"""Get Ban System API instance."""
logger.debug(
- f'Ban System check - enabled: {settings.is_ban_system_enabled()}, configured: {settings.is_ban_system_configured()}'
+ 'Ban System check enabled: configured',
+ is_ban_system_enabled=settings.is_ban_system_enabled(),
+ is_ban_system_configured=settings.is_ban_system_configured(),
)
- logger.debug(f'Ban System URL: {settings.get_ban_system_api_url()}')
+ logger.debug('Ban System URL', get_ban_system_api_url=settings.get_ban_system_api_url())
if not settings.is_ban_system_enabled():
raise HTTPException(
@@ -83,13 +85,13 @@ async def _api_request(api: BanSystemAPI, method: str, *args, **kwargs) -> Any:
func = getattr(api, method)
return await func(*args, **kwargs)
except BanSystemAPIError as e:
- logger.error(f'Ban System API error: {e}')
+ logger.error('Ban System API error', error=e)
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f'Ban System API error: {e.message}',
)
except Exception as e:
- logger.error(f'Ban System unexpected error: {e}')
+ logger.error('Ban System unexpected error', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Internal error: {e!s}',
@@ -133,7 +135,7 @@ async def get_stats(
api = _get_ban_api()
data = await _api_request(api, 'get_stats')
- logger.debug(f'Ban System raw stats: {data}')
+ logger.debug('Ban System raw stats', data=data)
# Extract punishment stats
punishment_stats = data.get('punishment_stats') or {}
@@ -364,7 +366,7 @@ async def unban_user(
api = _get_ban_api()
try:
await _api_request(api, 'enable_user', user_id=user_id)
- logger.info(f'Admin {admin.id} unbanned user {user_id} in Ban System')
+ logger.info('Admin unbanned user in Ban System', admin_id=admin.id, user_id=user_id)
return UnbanResponse(success=True, message='User unbanned successfully')
except HTTPException:
raise
@@ -387,7 +389,7 @@ async def ban_user(
minutes=request.minutes,
reason=request.reason,
)
- logger.info(f'Admin {admin.id} banned user {request.username}: {request.reason}')
+ logger.info('Admin banned user', admin_id=admin.id, username=request.username, reason=request.reason)
return UnbanResponse(success=True, message='User banned successfully')
except HTTPException:
raise
@@ -819,7 +821,7 @@ async def set_setting(
api = _get_ban_api()
data = await _api_request(api, 'set_setting', key=key, value=value)
- logger.info(f'Admin {admin.id} changed Ban System setting {key} to {value}')
+ logger.info('Admin changed Ban System setting to', admin_id=admin.id, key=key, value=value)
return _parse_setting_response(key, data)
@@ -833,7 +835,7 @@ async def toggle_setting(
api = _get_ban_api()
data = await _api_request(api, 'toggle_setting', key=key)
- logger.info(f'Admin {admin.id} toggled Ban System setting {key}')
+ logger.info('Admin toggled Ban System setting', admin_id=admin.id, key=key)
return _parse_setting_response(key, data, default_type='bool')
@@ -850,7 +852,7 @@ async def whitelist_add(
api = _get_ban_api()
try:
await _api_request(api, 'whitelist_add', username=request.username)
- logger.info(f'Admin {admin.id} added {request.username} to Ban System whitelist')
+ logger.info('Admin added to Ban System whitelist', admin_id=admin.id, username=request.username)
return UnbanResponse(success=True, message=f'User {request.username} added to whitelist')
except HTTPException:
raise
@@ -867,7 +869,7 @@ async def whitelist_remove(
api = _get_ban_api()
try:
await _api_request(api, 'whitelist_remove', username=request.username)
- logger.info(f'Admin {admin.id} removed {request.username} from Ban System whitelist')
+ logger.info('Admin removed from Ban System whitelist', admin_id=admin.id, username=request.username)
return UnbanResponse(success=True, message=f'User {request.username} removed from whitelist')
except HTTPException:
raise
diff --git a/app/cabinet/routes/admin_broadcasts.py b/app/cabinet/routes/admin_broadcasts.py
index 950c2690..099b1228 100644
--- a/app/cabinet/routes/admin_broadcasts.py
+++ b/app/cabinet/routes/admin_broadcasts.py
@@ -1,8 +1,8 @@
"""Admin routes for broadcasts in cabinet."""
-import logging
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import distinct, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -40,7 +40,7 @@ from ..schemas.broadcasts import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/broadcasts', tags=['Cabinet Admin Broadcasts'])
@@ -255,7 +255,7 @@ async def get_filters(
try:
count = await get_target_users_count(db, key)
except Exception as e:
- logger.warning(f'Failed to get count for filter {key}: {e}')
+ logger.warning('Failed to get count for filter', key=key, error=e)
count = 0
filters.append(
BroadcastFilter(
@@ -272,7 +272,7 @@ async def get_filters(
try:
count = await get_target_users_count(db, key)
except Exception as e:
- logger.warning(f'Failed to get count for custom filter {key}: {e}')
+ logger.warning('Failed to get count for custom filter', key=key, error=e)
count = 0
custom_filters.append(
BroadcastFilter(
@@ -367,7 +367,7 @@ async def preview_broadcast(
try:
count = await get_target_users_count(db, request.target)
except Exception as e:
- logger.error(f'Failed to get count for target {request.target}: {e}')
+ logger.error('Failed to get count for target', target=request.target, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to count recipients',
@@ -450,7 +450,9 @@ async def create_broadcast(
await broadcast_service.start_broadcast(broadcast.id, config)
await db.refresh(broadcast)
- logger.info(f"Admin {admin.id} created broadcast {broadcast.id} for target '{request.target}'")
+ logger.info(
+ 'Admin created broadcast for target', admin_id=admin.id, broadcast_id=broadcast.id, target=request.target
+ )
return _serialize_broadcast(broadcast)
@@ -494,7 +496,7 @@ async def get_email_filters(
try:
count = await _get_email_filter_count(db, key)
except Exception as e:
- logger.warning(f'Failed to get count for email filter {key}: {e}')
+ logger.warning('Failed to get count for email filter', key=key, error=e)
count = 0
filters.append(
@@ -532,7 +534,7 @@ async def preview_email_broadcast(
try:
count = await _get_email_filter_count(db, request.target)
except Exception as e:
- logger.error(f'Failed to get email count for target {request.target}: {e}')
+ logger.error('Failed to get email count for target', target=request.target, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to count email recipients',
@@ -661,7 +663,13 @@ async def create_combined_broadcast(
await db.refresh(broadcast)
- logger.info(f"Admin {admin.id} created {request.channel} broadcast {broadcast.id} for target '{request.target}'")
+ logger.info(
+ 'Admin created broadcast for target',
+ admin_id=admin.id,
+ channel=request.channel,
+ broadcast_id=broadcast.id,
+ target=request.target,
+ )
return _serialize_broadcast(broadcast)
@@ -721,6 +729,6 @@ async def stop_broadcast(
await db.commit()
await db.refresh(broadcast)
- logger.info(f'Admin {admin.id} stopped broadcast {broadcast_id}')
+ logger.info('Admin stopped broadcast', admin_id=admin.id, broadcast_id=broadcast_id)
return _serialize_broadcast(broadcast)
diff --git a/app/cabinet/routes/admin_button_styles.py b/app/cabinet/routes/admin_button_styles.py
index 9b039431..d67fbd4a 100644
--- a/app/cabinet/routes/admin_button_styles.py
+++ b/app/cabinet/routes/admin_button_styles.py
@@ -1,8 +1,8 @@
"""Admin routes for per-section cabinet button style configuration."""
import json
-import logging
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from app.utils.button_styles_cache import (
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/button-styles', tags=['Admin Button Styles'])
@@ -234,7 +234,9 @@ async def update_button_styles(
# Refresh in-process cache
await load_button_styles_cache()
- logger.info('Admin %s updated button styles for sections: %s', admin.telegram_id, changed_sections)
+ logger.info(
+ 'Admin updated button styles for sections', telegram_id=admin.telegram_id, changed_sections=changed_sections
+ )
return _build_response(current)
@@ -248,6 +250,6 @@ async def reset_button_styles(
await _set_setting_value(db, BUTTON_STYLES_KEY, json.dumps(DEFAULT_BUTTON_STYLES))
await load_button_styles_cache()
- logger.info('Admin %s reset button styles to defaults', admin.telegram_id)
+ logger.info('Admin reset button styles to defaults', telegram_id=admin.telegram_id)
return _build_response(DEFAULT_BUTTON_STYLES)
diff --git a/app/cabinet/routes/admin_campaigns.py b/app/cabinet/routes/admin_campaigns.py
index 117a7606..5e922b1e 100644
--- a/app/cabinet/routes/admin_campaigns.py
+++ b/app/cabinet/routes/admin_campaigns.py
@@ -1,7 +1,6 @@
"""Admin routes for managing advertising campaigns in cabinet."""
-import logging
-
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -45,7 +44,7 @@ from ..schemas.campaigns import (
from ..schemas.tariffs import TariffListItem
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/campaigns', tags=['Cabinet Admin Campaigns'])
@@ -378,7 +377,7 @@ async def create_new_campaign(
# Reload to get tariff relationship
campaign = await get_campaign_by_id(db, campaign.id)
- logger.info(f'Admin {admin.id} created campaign {campaign.id}: {campaign.name}')
+ logger.info('Admin created campaign', admin_id=admin.id, campaign_id=campaign.id, campaign_name=campaign.name)
return await get_campaign(campaign.id, admin, db)
@@ -447,7 +446,7 @@ async def update_existing_campaign(
if updates:
await update_campaign(db, campaign, **updates)
- logger.info(f'Admin {admin.id} updated campaign {campaign_id}')
+ logger.info('Admin updated campaign', admin_id=admin.id, campaign_id=campaign_id)
return await get_campaign(campaign_id, admin, db)
@@ -475,7 +474,7 @@ async def delete_existing_campaign(
)
await delete_campaign(db, campaign)
- logger.info(f'Admin {admin.id} deleted campaign {campaign_id}: {campaign.name}')
+ logger.info('Admin deleted campaign', admin_id=admin.id, campaign_id=campaign_id, campaign_name=campaign.name)
return {'message': 'Campaign deleted successfully'}
@@ -498,7 +497,7 @@ async def toggle_campaign(
await update_campaign(db, campaign, is_active=new_status)
status_text = 'activated' if new_status else 'deactivated'
- logger.info(f'Admin {admin.id} {status_text} campaign {campaign_id}')
+ logger.info('Admin campaign', admin_id=admin.id, status_text=status_text, campaign_id=campaign_id)
return CampaignToggleResponse(
id=campaign_id,
diff --git a/app/cabinet/routes/admin_email_templates.py b/app/cabinet/routes/admin_email_templates.py
index 4e4fdd02..01109403 100644
--- a/app/cabinet/routes/admin_email_templates.py
+++ b/app/cabinet/routes/admin_email_templates.py
@@ -1,9 +1,9 @@
"""Admin routes for managing email notification templates."""
import asyncio
-import logging
from typing import Any
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from ..services.email_template_overrides import (
from ..services.email_templates import EmailNotificationTemplates
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/email-templates', tags=['Admin Email Templates'])
@@ -505,10 +505,7 @@ async def update_template(
)
logger.info(
- 'Админ %s обновил email шаблон %s/%s',
- admin.id,
- notification_type,
- language,
+ 'Админ обновил email шаблон /', admin_id=admin.id, notification_type=notification_type, language=language
)
return {'status': 'ok', 'template': result}
@@ -533,10 +530,10 @@ async def reset_template(
if deleted:
logger.info(
- 'Админ %s сбросил email шаблон %s/%s к дефолту',
- admin.id,
- notification_type,
- language,
+ 'Админ сбросил email шаблон / к дефолту',
+ admin_id=admin.id,
+ notification_type=notification_type,
+ language=language,
)
return {'status': 'ok', 'was_custom': deleted}
@@ -657,7 +654,7 @@ async def send_test_email(
body_html=body_html,
)
except Exception as e:
- logger.error('Ошибка отправки тестового email: %s', e)
+ logger.error('Ошибка отправки тестового email', e=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Failed to send test email: {e!s}',
@@ -670,11 +667,11 @@ async def send_test_email(
)
logger.info(
- 'Админ %s отправил тестовый email %s/%s на %s',
- admin.id,
- notification_type,
- language,
- to_email,
+ 'Админ отправил тестовый email / на',
+ admin_id=admin.id,
+ notification_type=notification_type,
+ language=language,
+ to_email=to_email,
)
return {'status': 'ok', 'sent_to': to_email}
diff --git a/app/cabinet/routes/admin_payment_methods.py b/app/cabinet/routes/admin_payment_methods.py
index 4e6de175..7de07d48 100644
--- a/app/cabinet/routes/admin_payment_methods.py
+++ b/app/cabinet/routes/admin_payment_methods.py
@@ -1,8 +1,8 @@
"""Admin routes for payment method configuration in cabinet."""
-import logging
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from app.services.payment_method_config_service import (
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/payment-methods', tags=['Cabinet Admin Payment Methods'])
@@ -168,7 +168,7 @@ async def update_payment_methods_order(
):
"""Batch update sort order for payment methods."""
await update_sort_order(db, request.method_ids)
- logger.info(f'Admin {admin.id} updated payment methods order: {request.method_ids}')
+ logger.info('Admin updated payment methods order', admin_id=admin.id, method_ids=request.method_ids)
return {'success': True}
@@ -222,7 +222,7 @@ async def update_payment_method(
detail=f'Payment method not found: {method_id}',
)
- logger.info(f'Admin {admin.id} updated payment method config: {method_id}')
+ logger.info('Admin updated payment method config', admin_id=admin.id, method_id=method_id)
defaults = _get_method_defaults()
return _enrich_config(config, defaults)
diff --git a/app/cabinet/routes/admin_payments.py b/app/cabinet/routes/admin_payments.py
index 3b7f7ab3..10dbf991 100644
--- a/app/cabinet/routes/admin_payments.py
+++ b/app/cabinet/routes/admin_payments.py
@@ -1,9 +1,9 @@
"""Admin routes for payment verification in cabinet."""
-import logging
import math
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,7 +22,7 @@ from app.services.payment_verification_service import (
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/payments', tags=['Cabinet Admin Payments'])
@@ -406,7 +406,14 @@ async def check_payment_status(
if status_changed:
_, new_status_text = _get_status_info(updated)
message = f'Статус обновлён: {new_status_text}'
- logger.info(f'Admin {admin.id} checked payment {method}/{payment_id}: {old_status} -> {updated.status}')
+ logger.info(
+ 'Admin checked payment /',
+ admin_id=admin.id,
+ method=method,
+ payment_id=payment_id,
+ old_status=old_status,
+ status=updated.status,
+ )
else:
message = 'Статус не изменился'
diff --git a/app/cabinet/routes/admin_pinned_messages.py b/app/cabinet/routes/admin_pinned_messages.py
index 46776142..16f70839 100644
--- a/app/cabinet/routes/admin_pinned_messages.py
+++ b/app/cabinet/routes/admin_pinned_messages.py
@@ -1,9 +1,9 @@
"""Admin routes for pinned messages in cabinet."""
-import logging
import time
from datetime import datetime
+import structlog
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
@@ -34,7 +34,7 @@ from ..schemas.pinned_messages import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/pinned-messages', tags=['Cabinet Admin Pinned Messages'])
@@ -186,7 +186,9 @@ async def create_pinned_message(
if payload.broadcast:
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
- logger.info(f'Admin {admin.id} created pinned message #{msg.id} (broadcast={payload.broadcast})')
+ logger.info(
+ 'Admin created pinned message # (broadcast=)', admin_id=admin.id, message_id=msg.id, broadcast=payload.broadcast
+ )
return PinnedMessageBroadcastResponse(
message=_serialize_pinned_message(msg),
@@ -229,7 +231,7 @@ async def update_pinned_message(
await db.commit()
await db.refresh(msg)
- logger.info(f'Admin {admin.id} updated pinned message #{message_id}')
+ logger.info('Admin updated pinned message #', admin_id=admin.id, message_id=message_id)
return _serialize_pinned_message(msg)
@@ -273,7 +275,7 @@ async def deactivate_active_message(
if not msg:
return None
- logger.info(f'Admin {admin.id} deactivated pinned message #{msg.id}')
+ logger.info('Admin deactivated pinned message #', admin_id=admin.id, message_id=msg.id)
return _serialize_pinned_message(msg)
@@ -288,7 +290,12 @@ async def unpin_active_message(
unpinned_count, failed_count, was_active = await unpin_active_pinned_message(_get_bot(), db)
if was_active:
- logger.info(f'Admin {admin.id} unpinned active message: unpinned={unpinned_count}, failed={failed_count}')
+ logger.info(
+ 'Admin unpinned active message: unpinned=, failed',
+ admin_id=admin.id,
+ unpinned_count=unpinned_count,
+ failed_count=failed_count,
+ )
return PinnedMessageUnpinResponse(
unpinned_count=unpinned_count,
@@ -339,7 +346,9 @@ async def activate_pinned_message(
if broadcast:
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
- logger.info(f'Admin {admin.id} activated pinned message #{message_id} (broadcast={broadcast})')
+ logger.info(
+ 'Admin activated pinned message # (broadcast=)', admin_id=admin.id, message_id=message_id, broadcast=broadcast
+ )
return PinnedMessageBroadcastResponse(
message=_serialize_pinned_message(msg),
@@ -364,7 +373,13 @@ async def broadcast_message(
sent_count, failed_count = await broadcast_pinned_message(_get_bot(), db, msg)
- logger.info(f'Admin {admin.id} broadcast pinned message #{message_id}: sent={sent_count}, failed={failed_count}')
+ logger.info(
+ 'Admin broadcast pinned message #: sent=, failed',
+ admin_id=admin.id,
+ message_id=message_id,
+ sent_count=sent_count,
+ failed_count=failed_count,
+ )
return PinnedMessageBroadcastResponse(
message=_serialize_pinned_message(msg),
@@ -394,4 +409,4 @@ async def delete_pinned_message(
await db.delete(msg)
await db.commit()
- logger.info(f'Admin {admin.id} deleted pinned message #{message_id}')
+ logger.info('Admin deleted pinned message #', admin_id=admin.id, message_id=message_id)
diff --git a/app/cabinet/routes/admin_promo_offers.py b/app/cabinet/routes/admin_promo_offers.py
index ae47063c..43caf556 100644
--- a/app/cabinet/routes/admin_promo_offers.py
+++ b/app/cabinet/routes/admin_promo_offers.py
@@ -3,10 +3,10 @@
from __future__ import annotations
import asyncio
-import logging
from datetime import datetime
from typing import Any
+import structlog
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
@@ -37,7 +37,7 @@ from app.utils.miniapp_buttons import build_miniapp_or_callback_button
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/promo-offers', tags=['Admin Promo Offers'])
@@ -430,7 +430,7 @@ async def _send_promo_notifications(
async def send_single(user: User, offer: DiscountOffer) -> bool:
# Skip email-only users (no telegram_id)
if not user.telegram_id:
- logger.debug(f'Skipping promo notification for email-only user {user.id}')
+ logger.debug('Skipping promo notification for email-only user', user_id=user.id)
return False
async with semaphore:
@@ -459,18 +459,10 @@ async def _send_promo_notifications(
)
return True
except (TelegramForbiddenError, TelegramBadRequest) as exc:
- logger.warning(
- 'Failed to send promo notification to user %s: %s',
- user.telegram_id,
- exc,
- )
+ logger.warning('Failed to send promo notification to user', telegram_id=user.telegram_id, exc=exc)
return False
except Exception as exc:
- logger.error(
- 'Error sending promo notification to user %s: %s',
- user.telegram_id,
- exc,
- )
+ logger.error('Error sending promo notification to user', telegram_id=user.telegram_id, exc=exc)
return False
# Send in batches
diff --git a/app/cabinet/routes/admin_remnawave.py b/app/cabinet/routes/admin_remnawave.py
index f773d3e9..8d7be615 100644
--- a/app/cabinet/routes/admin_remnawave.py
+++ b/app/cabinet/routes/admin_remnawave.py
@@ -1,9 +1,9 @@
"""Admin routes for RemnaWave management in cabinet."""
-import logging
from datetime import datetime, timedelta
from typing import Any
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
@@ -76,7 +76,7 @@ except Exception:
remnawave_sync_service = None
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/remnawave', tags=['Cabinet Admin RemnaWave'])
@@ -380,7 +380,9 @@ async def perform_node_action(
}
if success:
- logger.info(f'Admin {admin.telegram_id} performed {payload.action} on node {node_uuid}')
+ logger.info(
+ 'Admin performed on node', telegram_id=admin.telegram_id, action=payload.action, node_uuid=node_uuid
+ )
return NodeActionResponse(
success=True,
message=messages.get(payload.action, 'Action completed'),
@@ -403,7 +405,7 @@ async def restart_all_nodes(
success = await service.restart_all_nodes()
if success:
- logger.info(f'Admin {admin.telegram_id} restarted all nodes')
+ logger.info('Admin restarted all nodes', telegram_id=admin.telegram_id)
return NodeActionResponse(success=True, message='All nodes restart initiated')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -510,7 +512,9 @@ async def create_squad(
squad_uuid = await service.create_squad(payload.name, payload.inbound_uuids)
if squad_uuid:
- logger.info(f'Admin {admin.telegram_id} created squad {payload.name} ({squad_uuid})')
+ logger.info(
+ 'Admin created squad', telegram_id=admin.telegram_id, payload_name=payload.name, squad_uuid=squad_uuid
+ )
return SquadOperationResponse(
success=True,
message='Squad created successfully',
@@ -545,7 +549,7 @@ async def update_squad(
)
if success:
- logger.info(f'Admin {admin.telegram_id} updated squad {squad_uuid}')
+ logger.info('Admin updated squad', telegram_id=admin.telegram_id, squad_uuid=squad_uuid)
return SquadOperationResponse(success=True, message='Squad updated')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -594,7 +598,7 @@ async def perform_squad_action(
message = 'Inbounds updated' if success else 'Failed to update inbounds'
if success:
- logger.info(f'Admin {admin.telegram_id} performed {action} on squad {squad_uuid}')
+ logger.info('Admin performed on squad', telegram_id=admin.telegram_id, action=action, squad_uuid=squad_uuid)
return SquadOperationResponse(success=success, message=message)
@@ -611,7 +615,7 @@ async def delete_squad(
success = await service.delete_squad(squad_uuid)
if success:
- logger.info(f'Admin {admin.telegram_id} deleted squad {squad_uuid}')
+ logger.info('Admin deleted squad', telegram_id=admin.telegram_id, squad_uuid=squad_uuid)
return SquadOperationResponse(success=True, message='Squad deleted')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -699,7 +703,9 @@ async def migrate_squad_users(
error=result.get('error'),
)
- logger.info(f'Admin {admin.telegram_id} migrated users from {source_uuid} to {target_uuid}')
+ logger.info(
+ 'Admin migrated users from to', telegram_id=admin.telegram_id, source_uuid=source_uuid, target_uuid=target_uuid
+ )
return MigrationResponse(
success=True,
@@ -782,14 +788,14 @@ async def toggle_auto_sync(
if payload.enabled and not current_status.enabled:
# Enable - would need to update settings and refresh schedule
remnawave_sync_service.schedule_refresh(run_immediately=True)
- logger.info(f'Admin {admin.telegram_id} enabled auto sync')
+ logger.info('Admin enabled auto sync', telegram_id=admin.telegram_id)
return SyncResponse(
success=True,
message='Auto sync enabled and scheduled',
)
if not payload.enabled and current_status.enabled:
# Disable - would need to update settings and stop scheduler
- logger.info(f'Admin {admin.telegram_id} disabled auto sync')
+ logger.info('Admin disabled auto sync', telegram_id=admin.telegram_id)
return SyncResponse(
success=True,
message='Auto sync setting change requested. Restart may be required.',
@@ -811,7 +817,7 @@ async def run_auto_sync_now(
detail='Auto sync service is not available',
)
- logger.info(f'Admin {admin.telegram_id} triggered manual sync')
+ logger.info('Admin triggered manual sync', telegram_id=admin.telegram_id)
result = await remnawave_sync_service.run_sync_now(reason='manual')
return AutoSyncRunResponse(
@@ -839,7 +845,7 @@ async def sync_from_panel(
try:
stats = await service.sync_users_from_panel(db, payload.mode)
- logger.info(f'Admin {admin.telegram_id} synced from panel (mode: {payload.mode})')
+ logger.info('Admin synced from panel (mode: )', telegram_id=admin.telegram_id, mode=payload.mode)
return SyncResponse(
success=True,
message='Sync from panel completed',
@@ -862,7 +868,7 @@ async def sync_to_panel(
_ensure_configured(service)
stats = await service.sync_users_to_panel(db)
- logger.info(f'Admin {admin.telegram_id} synced to panel')
+ logger.info('Admin synced to panel', telegram_id=admin.telegram_id)
return SyncResponse(
success=True,
@@ -892,9 +898,15 @@ async def sync_servers(
try:
await cache.delete_pattern('available_countries*')
except Exception as e:
- logger.warning(f'Failed to clear countries cache: {e}')
+ logger.warning('Failed to clear countries cache', error=e)
- logger.info(f'Admin {admin.telegram_id} synced servers: created={created}, updated={updated}, removed={removed}')
+ logger.info(
+ 'Admin synced servers: created=, updated=, removed',
+ telegram_id=admin.telegram_id,
+ created=created,
+ updated=updated,
+ removed=removed,
+ )
return SyncResponse(
success=True,
@@ -918,7 +930,7 @@ async def validate_subscriptions(
_ensure_configured(service)
stats = await service.validate_and_fix_subscriptions(db)
- logger.info(f'Admin {admin.telegram_id} validated subscriptions')
+ logger.info('Admin validated subscriptions', telegram_id=admin.telegram_id)
return SyncResponse(
success=True,
@@ -937,7 +949,7 @@ async def cleanup_subscriptions(
_ensure_configured(service)
stats = await service.cleanup_orphaned_subscriptions(db)
- logger.info(f'Admin {admin.telegram_id} cleaned up subscriptions')
+ logger.info('Admin cleaned up subscriptions', telegram_id=admin.telegram_id)
return SyncResponse(
success=True,
@@ -956,7 +968,7 @@ async def sync_subscription_statuses(
_ensure_configured(service)
stats = await service.sync_subscription_statuses(db)
- logger.info(f'Admin {admin.telegram_id} synced subscription statuses')
+ logger.info('Admin synced subscription statuses', telegram_id=admin.telegram_id)
return SyncResponse(
success=True,
diff --git a/app/cabinet/routes/admin_servers.py b/app/cabinet/routes/admin_servers.py
index 48072276..898eb432 100644
--- a/app/cabinet/routes/admin_servers.py
+++ b/app/cabinet/routes/admin_servers.py
@@ -1,7 +1,6 @@
"""Admin routes for managing servers in cabinet."""
-import logging
-
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import String, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -31,7 +30,7 @@ from ..schemas.servers import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/servers', tags=['Cabinet Admin Servers'])
@@ -184,7 +183,7 @@ async def update_existing_server(
if request.promo_group_ids is not None:
await update_server_squad_promo_groups(db, server_id, request.promo_group_ids)
- logger.info(f'Admin {admin.id} updated server {server_id}')
+ logger.info('Admin updated server', admin_id=admin.id, server_id=server_id)
return await get_server(server_id, admin, db)
@@ -207,7 +206,7 @@ async def toggle_server(
await update_server_squad(db, server_id, is_available=new_status)
status_text = 'enabled' if new_status else 'disabled'
- logger.info(f'Admin {admin.id} {status_text} server {server_id}')
+ logger.info('Admin server', admin_id=admin.id, status_text=status_text, server_id=server_id)
return ServerToggleResponse(
id=server_id,
@@ -234,7 +233,7 @@ async def toggle_server_trial(
await update_server_squad(db, server_id, is_trial_eligible=new_status)
status_text = 'enabled for trial' if new_status else 'disabled for trial'
- logger.info(f'Admin {admin.id} {status_text} server {server_id}')
+ logger.info('Admin server', admin_id=admin.id, status_text=status_text, server_id=server_id)
return ServerTrialToggleResponse(
id=server_id,
@@ -311,7 +310,7 @@ async def sync_servers(
# Sync with database
created, updated, removed = await sync_with_remnawave(db, squads)
- logger.info(f'Admin {admin.id} synced servers: +{created} ~{updated} -{removed}')
+ logger.info('Admin synced servers: + ~', admin_id=admin.id, created=created, updated=updated, removed=removed)
return ServerSyncResponse(
created=created,
@@ -323,7 +322,7 @@ async def sync_servers(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Failed to sync servers: {e}')
+ logger.error('Failed to sync servers', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Sync failed: {e!s}',
diff --git a/app/cabinet/routes/admin_settings.py b/app/cabinet/routes/admin_settings.py
index a2fa69fb..de672f57 100644
--- a/app/cabinet/routes/admin_settings.py
+++ b/app/cabinet/routes/admin_settings.py
@@ -1,8 +1,8 @@
"""Admin settings routes for cabinet - system configuration management."""
-import logging
from typing import Any
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +16,7 @@ from app.services.system_settings_service import (
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/settings', tags=['Admin Settings'])
@@ -248,7 +248,7 @@ async def update_setting(
raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error
await db.commit()
- logger.info(f'Admin {admin.telegram_id} updated setting {key} to {value}')
+ logger.info('Admin updated setting to', telegram_id=admin.telegram_id, key=key, value=value)
return _serialize_definition(definition)
@@ -270,5 +270,5 @@ async def reset_setting(
raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error
await db.commit()
- logger.info(f'Admin {admin.telegram_id} reset setting {key}')
+ logger.info('Admin reset setting', telegram_id=admin.telegram_id, key=key)
return _serialize_definition(definition)
diff --git a/app/cabinet/routes/admin_stats.py b/app/cabinet/routes/admin_stats.py
index 383bfd36..deda96c9 100644
--- a/app/cabinet/routes/admin_stats.py
+++ b/app/cabinet/routes/admin_stats.py
@@ -1,10 +1,10 @@
"""Admin routes for statistics dashboard in cabinet."""
-import logging
import sys
import time
from datetime import datetime, timedelta
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import and_, func, select
@@ -29,7 +29,7 @@ from app.services.version_service import version_service
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
_start_time = time.time()
@@ -317,7 +317,7 @@ async def get_dashboard_stats(
)
except Exception as e:
- logger.error(f'Failed to get dashboard stats: {e}')
+ logger.error('Failed to get dashboard stats', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load dashboard statistics',
@@ -349,7 +349,7 @@ async def get_system_info(
subscriptions_active=subscriptions_active,
)
except Exception as e:
- logger.error(f'Failed to get system info: {e}')
+ logger.error('Failed to get system info', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load system information',
@@ -364,7 +364,7 @@ async def get_nodes_status(
try:
return await _get_nodes_overview()
except Exception as e:
- logger.error(f'Failed to get nodes status: {e}')
+ logger.error('Failed to get nodes status', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load nodes status',
@@ -382,7 +382,7 @@ async def restart_node(
success = await service.manage_node(node_uuid, 'restart')
if success:
- logger.info(f'Admin {admin.id} restarted node {node_uuid}')
+ logger.info('Admin restarted node', admin_id=admin.id, node_uuid=node_uuid)
return {'success': True, 'message': 'Node restart initiated'}
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -391,7 +391,7 @@ async def restart_node(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Failed to restart node {node_uuid}: {e}')
+ logger.error('Failed to restart node', node_uuid=node_uuid, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to restart node',
@@ -420,7 +420,7 @@ async def toggle_node(
success = await service.manage_node(node_uuid, action)
if success:
- logger.info(f'Admin {admin.id} {action}d node {node_uuid}')
+ logger.info('Admin d node', admin_id=admin.id, action=action, node_uuid=node_uuid)
return {'success': True, 'message': f'Node {action}d', 'is_disabled': not is_disabled}
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -429,7 +429,7 @@ async def toggle_node(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Failed to toggle node {node_uuid}: {e}')
+ logger.error('Failed to toggle node', node_uuid=node_uuid, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to toggle node',
@@ -480,7 +480,7 @@ async def _get_nodes_overview() -> NodesOverview:
nodes=node_statuses,
)
except Exception as e:
- logger.warning(f'Failed to get nodes from RemnaWave: {e}')
+ logger.warning('Failed to get nodes from RemnaWave', error=e)
# Return empty data if RemnaWave is unavailable
return NodesOverview(
total=0,
@@ -560,7 +560,9 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
)
purchased_month = month_result.scalar() or 0
- logger.info(f"📊 Тариф '{tariff.name}': активных={active_count}, триал={trial_count}")
+ logger.info(
+ '📊 Тариф активных=, триал', tariff_name=tariff.name, active_count=active_count, trial_count=trial_count
+ )
tariff_items.append(
TariffStatItem(
@@ -576,7 +578,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
total_tariff_subscriptions += active_count
- logger.info(f'📊 Всего подписок по тарифам: {total_tariff_subscriptions}')
+ logger.info('📊 Всего подписок по тарифам', total_tariff_subscriptions=total_tariff_subscriptions)
return TariffStats(
tariffs=tariff_items,
@@ -584,7 +586,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
)
except Exception as e:
- logger.error(f'Failed to get tariff stats: {e}', exc_info=True)
+ logger.error('Failed to get tariff stats', error=e, exc_info=True)
return None
@@ -800,7 +802,7 @@ async def get_top_referrers(
)
except Exception as e:
- logger.error(f'Failed to get top referrers: {e}', exc_info=True)
+ logger.error('Failed to get top referrers', error=e, exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load referrers statistics',
@@ -857,7 +859,7 @@ async def get_top_campaigns(
)
except Exception as e:
- logger.error(f'Failed to get top campaigns: {e}', exc_info=True)
+ logger.error('Failed to get top campaigns', error=e, exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load campaigns statistics',
@@ -996,7 +998,7 @@ async def get_recent_payments(
)
except Exception as e:
- logger.error(f'Failed to get recent payments: {e}', exc_info=True)
+ logger.error('Failed to get recent payments', error=e, exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load recent payments',
diff --git a/app/cabinet/routes/admin_tariffs.py b/app/cabinet/routes/admin_tariffs.py
index 87fdd783..09d53987 100644
--- a/app/cabinet/routes/admin_tariffs.py
+++ b/app/cabinet/routes/admin_tariffs.py
@@ -1,7 +1,6 @@
"""Admin routes for managing tariffs in cabinet."""
-import logging
-
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -38,7 +37,7 @@ from ..schemas.tariffs import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/tariffs', tags=['Cabinet Admin Tariffs'])
@@ -169,7 +168,7 @@ async def update_tariff_order(
await reorder_tariffs(db, request.tariff_ids)
await db.commit()
- logger.info(f'Admin {admin.id} updated tariff order: {request.tariff_ids}')
+ logger.info('Admin updated tariff order', admin_id=admin.id, tariff_ids=request.tariff_ids)
return {'message': 'Tariff order updated successfully'}
@@ -295,7 +294,7 @@ async def create_new_tariff(
traffic_reset_mode=request.traffic_reset_mode,
)
- logger.info(f'Admin {admin.id} created tariff {tariff.id}: {tariff.name}')
+ logger.info('Admin created tariff', admin_id=admin.id, tariff_id=tariff.id, tariff_name=tariff.name)
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
@@ -390,7 +389,7 @@ async def update_existing_tariff(
if request.promo_group_ids is not None:
await set_tariff_promo_groups(db, tariff, request.promo_group_ids)
- logger.info(f'Admin {admin.id} updated tariff {tariff_id}')
+ logger.info('Admin updated tariff', admin_id=admin.id, tariff_id=tariff_id)
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
@@ -414,7 +413,13 @@ async def delete_existing_tariff(
subs_count = await get_tariff_subscriptions_count(db, tariff_id)
await delete_tariff(db, tariff)
- logger.info(f'Admin {admin.id} deleted tariff {tariff_id}: {tariff.name} (affected subscriptions: {subs_count})')
+ logger.info(
+ 'Admin deleted tariff (affected subscriptions: )',
+ admin_id=admin.id,
+ tariff_id=tariff_id,
+ tariff_name=tariff.name,
+ subs_count=subs_count,
+ )
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
@@ -440,7 +445,7 @@ async def toggle_tariff(
await update_tariff(db, tariff, is_active=new_status)
status_text = 'activated' if new_status else 'deactivated'
- logger.info(f'Admin {admin.id} {status_text} tariff {tariff_id}')
+ logger.info('Admin tariff', admin_id=admin.id, status_text=status_text, tariff_id=tariff_id)
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
@@ -483,7 +488,7 @@ async def toggle_trial_tariff(
await update_tariff(db, tariff, is_trial_available=new_status)
status_text = 'set as trial' if new_status else 'removed from trial'
- logger.info(f'Admin {admin.id} {status_text} tariff {tariff_id}')
+ logger.info('Admin tariff', admin_id=admin.id, status_text=status_text, tariff_id=tariff_id)
return TariffTrialResponse(
id=tariff_id,
diff --git a/app/cabinet/routes/admin_tickets.py b/app/cabinet/routes/admin_tickets.py
index 040ddeb3..72e21b81 100644
--- a/app/cabinet/routes/admin_tickets.py
+++ b/app/cabinet/routes/admin_tickets.py
@@ -1,9 +1,9 @@
"""Admin tickets routes for cabinet."""
-import logging
import math
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field
from sqlalchemy import desc, func, select
@@ -20,7 +20,7 @@ from ..dependencies import get_cabinet_db, get_current_admin_user
from ..schemas.tickets import TicketMessageResponse
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/tickets', tags=['Cabinet Admin Tickets'])
@@ -317,7 +317,7 @@ async def update_ticket_settings(
env_file.write_text('\n'.join(new_lines) + '\n')
logger.info('Updated ticket settings in .env file')
except Exception as e:
- logger.warning(f'Failed to update .env file: {e}')
+ logger.warning('Failed to update .env file', error=e)
return TicketSettingsResponse(
sla_enabled=settings.SUPPORT_TICKET_SLA_ENABLED,
@@ -473,11 +473,11 @@ async def reply_to_ticket(
await notify_user_about_ticket_reply(bot, ticket, request.message, db)
except Exception as e:
- logger.warning(f'Failed to notify user about ticket reply: {e}')
+ logger.warning('Failed to notify user about ticket reply', error=e)
finally:
await bot.session.close()
except Exception as e:
- logger.warning(f'Failed to send Telegram notification: {e}')
+ logger.warning('Failed to send Telegram notification', error=e)
# Уведомить пользователя в кабинете
try:
@@ -488,7 +488,7 @@ async def reply_to_ticket(
# Отправить WebSocket уведомление
await notify_user_ticket_reply(ticket.user_id, ticket.id, (request.message or '')[:100])
except Exception as e:
- logger.warning(f'Failed to create cabinet notification for admin reply: {e}')
+ logger.warning('Failed to create cabinet notification for admin reply', error=e)
return _message_to_response(message)
diff --git a/app/cabinet/routes/admin_traffic.py b/app/cabinet/routes/admin_traffic.py
index ed67429e..9977b25b 100644
--- a/app/cabinet/routes/admin_traffic.py
+++ b/app/cabinet/routes/admin_traffic.py
@@ -3,10 +3,10 @@
import asyncio
import csv
import io
-import logging
import time
from datetime import UTC, datetime, timedelta
+import structlog
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
@@ -32,7 +32,7 @@ from ..schemas.traffic import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/traffic', tags=['Admin Traffic'])
@@ -110,7 +110,7 @@ async def _aggregate_traffic(
stats = await api.get_bandwidth_stats_node_users_legacy(node.uuid, start_str, end_str)
return node.uuid, stats
except Exception:
- logger.warning('Failed to get traffic for node %s', node.name, exc_info=True)
+ logger.warning('Failed to get traffic for node', node_name=node.name, exc_info=True)
return node.uuid, None
results = await asyncio.gather(*(fetch_node_users(n) for n in nodes))
@@ -685,7 +685,7 @@ async def export_traffic_csv(
caption=f'Traffic usage report ({period_label})\nUsers: {len(rows)}',
)
except Exception:
- logger.error('Failed to send CSV to admin %s', admin.telegram_id, exc_info=True)
+ logger.error('Failed to send CSV to admin', telegram_id=admin.telegram_id, exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to send CSV report. Please try again later.',
diff --git a/app/cabinet/routes/admin_updates.py b/app/cabinet/routes/admin_updates.py
index faa7565b..1e5ce846 100644
--- a/app/cabinet/routes/admin_updates.py
+++ b/app/cabinet/routes/admin_updates.py
@@ -1,9 +1,9 @@
"""Admin routes for version and release information."""
-import logging
from datetime import datetime, timedelta
import aiohttp
+import structlog
from fastapi import APIRouter, Depends
from pydantic import BaseModel
@@ -13,7 +13,7 @@ from app.services.version_service import version_service
from ..dependencies import get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/updates', tags=['Cabinet Admin Updates'])
@@ -76,15 +76,15 @@ async def _fetch_cabinet_releases(force: bool = False) -> list[dict]:
)
_cabinet_cache['releases'] = releases
_cabinet_last_check = datetime.now()
- logger.info('Fetched %d cabinet releases from GitHub', len(releases))
+ logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases))
return releases
- logger.warning('GitHub API returned status %d for cabinet releases', response.status)
+ logger.warning('GitHub API returned status for cabinet releases', response_status=response.status)
return _cabinet_cache.get('releases', [])
except TimeoutError:
logger.warning('Timeout fetching cabinet releases from GitHub')
return _cabinet_cache.get('releases', [])
except Exception as e:
- logger.error('Error fetching cabinet releases: %s', e)
+ logger.error('Error fetching cabinet releases', e=e)
return _cabinet_cache.get('releases', [])
diff --git a/app/cabinet/routes/admin_users.py b/app/cabinet/routes/admin_users.py
index 064a7f24..6fc13f6c 100644
--- a/app/cabinet/routes/admin_users.py
+++ b/app/cabinet/routes/admin_users.py
@@ -1,8 +1,8 @@
"""Admin routes for managing users in cabinet."""
-import logging
from datetime import datetime, timedelta
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import Integer, and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -91,7 +91,7 @@ from ..schemas.users import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/users', tags=['Cabinet Admin Users'])
@@ -219,7 +219,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
service = RemnaWaveService()
if not service.is_configured:
- logger.warning(f'Remnawave not configured, skipping panel sync for user {user.id}')
+ logger.warning('Remnawave not configured, skipping panel sync for user', user_id=user.id)
return {'skipped': True, 'reason': 'Remnawave not configured'}
is_active = (
@@ -260,7 +260,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
if panel_uuid:
existing_user = await api.get_user_by_uuid(panel_uuid)
if not existing_user:
- logger.warning(f'User {user.id} has stale remnawave_uuid {panel_uuid}, clearing')
+ logger.warning('User has stale remnawave_uuid clearing', user_id=user.id, panel_uuid=panel_uuid)
panel_uuid = None
user.remnawave_uuid = None
@@ -299,7 +299,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
try:
await api.update_user(**update_kwargs)
changes['action'] = 'updated'
- logger.info(f'Updated user {user.id} in Remnawave panel')
+ logger.info('Updated user in Remnawave panel', user_id=user.id)
except Exception as update_error:
if hasattr(update_error, 'status_code') and update_error.status_code == 404:
panel_uuid = None # Will create new
@@ -328,7 +328,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
subscription.subscription_url = new_panel_user.subscription_url
changes['action'] = 'created'
changes['panel_uuid'] = new_panel_user.uuid
- logger.info(f'Created user {user.id} in Remnawave panel: {new_panel_user.uuid}')
+ logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid)
user.last_remnawave_sync = datetime.utcnow()
await db.commit()
@@ -336,7 +336,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
return changes
except Exception as e:
- logger.error(f'Error syncing user {user.id} to panel: {e}')
+ logger.error('Error syncing user to panel', user_id=user.id, error=e)
return {'error': str(e)}
@@ -709,7 +709,7 @@ async def get_user_panel_info(
last_node_name = node.node_name
break
except Exception:
- logger.warning(f'Failed to resolve node name for user {user_id}')
+ logger.warning('Failed to resolve node name for user', user_id=user_id)
return UserPanelInfoResponse(
found=True,
@@ -728,7 +728,7 @@ async def get_user_panel_info(
)
except Exception as e:
- logger.error(f'Error getting panel info for user {user_id}: {e}')
+ logger.error('Error getting panel info for user', user_id=user_id, error=e)
return UserPanelInfoResponse(found=False)
@@ -812,7 +812,7 @@ async def get_user_node_usage(
return UserNodeUsageResponse(items=items, categories=categories)
except Exception as e:
- logger.error(f'Error getting node usage for user {user_id}: {e}')
+ logger.error('Error getting node usage for user', user_id=user_id, error=e)
return UserNodeUsageResponse(items=[])
@@ -877,8 +877,12 @@ async def update_user_balance(
await db.refresh(user)
logger.info(
- f'Admin {admin.id} updated balance for user {user_id}: '
- f'{old_balance} -> {user.balance_kopeks} ({request.amount_kopeks:+d})'
+ 'Admin updated balance for user',
+ admin_id=admin.id,
+ user_id=user_id,
+ old_balance=old_balance,
+ balance_kopeks=user.balance_kopeks,
+ amount_kopeks=format(request.amount_kopeks, '+d'),
)
return UpdateBalanceResponse(
@@ -962,7 +966,7 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, new_sub)
- logger.info(f'Admin {admin.id} created subscription for user {user_id}')
+ logger.info('Admin created subscription for user', admin_id=admin.id, user_id=user_id)
return UpdateSubscriptionResponse(
success=True,
@@ -989,7 +993,9 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} extended subscription for user {user_id} by {request.days} days')
+ logger.info(
+ 'Admin extended subscription for user by days', admin_id=admin.id, user_id=user_id, days=request.days
+ )
return UpdateSubscriptionResponse(
success=True,
@@ -1016,7 +1022,7 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} set end_date for user {user_id} subscription')
+ logger.info('Admin set end_date for user subscription', admin_id=admin.id, user_id=user_id)
return UpdateSubscriptionResponse(
success=True,
@@ -1050,7 +1056,7 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} changed tariff for user {user_id} to {tariff.name}')
+ logger.info('Admin changed tariff for user to', admin_id=admin.id, user_id=user_id, tariff_name=tariff.name)
return UpdateSubscriptionResponse(
success=True,
@@ -1071,7 +1077,7 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} updated traffic for user {user_id}')
+ logger.info('Admin updated traffic for user', admin_id=admin.id, user_id=user_id)
return UpdateSubscriptionResponse(
success=True,
@@ -1091,7 +1097,7 @@ async def update_user_subscription(
await db.refresh(subscription)
state = 'enabled' if request.autopay_enabled else 'disabled'
- logger.info(f'Admin {admin.id} {state} autopay for user {user_id}')
+ logger.info('Admin autopay for user', admin_id=admin.id, state=state, user_id=user_id)
return UpdateSubscriptionResponse(
success=True,
@@ -1108,7 +1114,7 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} cancelled subscription for user {user_id}')
+ logger.info('Admin cancelled subscription for user', admin_id=admin.id, user_id=user_id)
return UpdateSubscriptionResponse(
success=True,
@@ -1127,7 +1133,7 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} activated subscription for user {user_id}')
+ logger.info('Admin activated subscription for user', admin_id=admin.id, user_id=user_id)
return UpdateSubscriptionResponse(
success=True,
@@ -1151,7 +1157,7 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} added {request.traffic_gb} GB traffic for user {user_id}')
+ logger.info('Admin added traffic for user', admin_id=admin.id, traffic_gb=request.traffic_gb, user_id=user_id)
return UpdateSubscriptionResponse(
success=True,
@@ -1211,7 +1217,11 @@ async def update_user_subscription(
await _sync_subscription_to_panel(db, user, subscription)
logger.info(
- f'Admin {admin.id} removed traffic purchase {request.traffic_purchase_id} ({removed_gb} GB) for user {user_id}'
+ 'Admin removed traffic purchase ( GB) for user',
+ admin_id=admin.id,
+ traffic_purchase_id=request.traffic_purchase_id,
+ removed_gb=removed_gb,
+ user_id=user_id,
)
return UpdateSubscriptionResponse(
@@ -1234,7 +1244,9 @@ async def update_user_subscription(
# Sync to Remnawave panel
await _sync_subscription_to_panel(db, user, subscription)
- logger.info(f'Admin {admin.id} set device limit to {request.device_limit} for user {user_id}')
+ logger.info(
+ 'Admin set device limit to for user', admin_id=admin.id, device_limit=request.device_limit, user_id=user_id
+ )
return UpdateSubscriptionResponse(
success=True,
@@ -1384,7 +1396,7 @@ async def update_user_status(
if request.reason:
action += f' (reason: {request.reason})'
- logger.info(f'Admin {admin.id} changed status for user {user_id}: {action}')
+ logger.info('Admin changed status for user', admin_id=admin.id, user_id=user_id, action=action)
return UpdateUserStatusResponse(
success=True,
@@ -1449,8 +1461,11 @@ async def update_user_restrictions(
await db.refresh(user)
logger.info(
- f'Admin {admin.id} updated restrictions for user {user_id}: '
- f'topup={user.restriction_topup}, subscription={user.restriction_subscription}'
+ 'Admin updated restrictions for user topup=, subscription',
+ admin_id=admin.id,
+ user_id=user_id,
+ restriction_topup=user.restriction_topup,
+ restriction_subscription=user.restriction_subscription,
)
return UpdateRestrictionsResponse(
@@ -1501,7 +1516,11 @@ async def update_user_promo_group(
await db.refresh(user)
logger.info(
- f'Admin {admin.id} changed promo group for user {user_id}: {old_promo_group_id} -> {new_promo_group_id}'
+ 'Admin changed promo group for user',
+ admin_id=admin.id,
+ user_id=user_id,
+ old_promo_group_id=old_promo_group_id,
+ new_promo_group_id=new_promo_group_id,
)
return UpdatePromoGroupResponse(
@@ -1537,7 +1556,11 @@ async def update_user_referral_commission(
await db.commit()
logger.info(
- f'Admin {admin.id} changed referral commission for user {user_id}: {old_commission} -> {request.commission_percent}'
+ 'Admin changed referral commission for user',
+ admin_id=admin.id,
+ user_id=user_id,
+ old_commission=old_commission,
+ commission_percent=request.commission_percent,
)
return UpdateReferralCommissionResponse(
@@ -1600,7 +1623,7 @@ async def get_user_devices(
)
except Exception as e:
- logger.error(f'Error fetching devices for user {user_id}: {e}')
+ logger.error('Error fetching devices for user', user_id=user_id, error=e)
return UserDevicesResponse()
@@ -1627,12 +1650,12 @@ async def delete_user_device(
success = await api.remove_device(user.remnawave_uuid, hwid)
if success:
- logger.info(f'Admin {admin.id} deleted device {hwid} for user {user_id}')
+ logger.info('Admin deleted device for user', admin_id=admin.id, hwid=hwid, user_id=user_id)
return DeleteDeviceResponse(success=True, message='Device deleted', deleted_hwid=hwid)
return DeleteDeviceResponse(success=False, message='Failed to delete device')
except Exception as e:
- logger.error(f'Error deleting device {hwid} for user {user_id}: {e}')
+ logger.error('Error deleting device for user', hwid=hwid, user_id=user_id, error=e)
return DeleteDeviceResponse(success=False, message=str(e))
@@ -1672,11 +1695,11 @@ async def reset_user_devices(
except Exception:
pass
- logger.info(f'Admin {admin.id} reset devices for user {user_id}: {deleted}/{total}')
+ logger.info('Admin reset devices for user /', admin_id=admin.id, user_id=user_id, deleted=deleted, total=total)
return ResetDevicesResponse(success=True, message=f'Deleted {deleted}/{total} devices', deleted_count=deleted)
except Exception as e:
- logger.error(f'Error resetting devices for user {user_id}: {e}')
+ logger.error('Error resetting devices for user', user_id=user_id, error=e)
return ResetDevicesResponse(success=False, message=str(e))
@@ -1713,7 +1736,7 @@ async def delete_user(
action = 'permanently deleted'
reason_text = f' (reason: {request.reason})' if request.reason else ''
- logger.info(f'Admin {admin.id} {action} user {user_id}{reason_text}')
+ logger.info('Admin user', admin_id=admin.id, action=action, user_id=user_id, reason_text=reason_text)
return DeleteUserResponse(
success=True,
@@ -1756,7 +1779,7 @@ async def full_delete_user(
deleted_from_panel = request.delete_from_panel and user.remnawave_uuid is not None
reason_text = f' (reason: {request.reason})' if request.reason else ''
- logger.info(f'Admin {admin.id} fully deleted user {user_id}{reason_text}')
+ logger.info('Admin fully deleted user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
return FullDeleteUserResponse(
success=success,
@@ -1800,9 +1823,9 @@ async def reset_user_trial(
subscription_service = SubscriptionService()
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
- logger.info(f'Disabled Remnawave user {user.remnawave_uuid} for trial reset')
+ logger.info('Disabled Remnawave user for trial reset', remnawave_uuid=user.remnawave_uuid)
except Exception as e:
- logger.warning(f'Failed to disable Remnawave user during trial reset: {e}')
+ logger.warning('Failed to disable Remnawave user during trial reset', error=e)
# Delete subscription from database
from sqlalchemy import delete
@@ -1819,7 +1842,7 @@ async def reset_user_trial(
await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else ''
- logger.info(f'Admin {admin.id} reset trial for user {user_id}{reason_text}')
+ logger.info('Admin reset trial for user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
return ResetTrialResponse(
success=True,
@@ -1871,10 +1894,10 @@ async def reset_user_subscription(
subscription_service = SubscriptionService()
panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid)
if panel_deactivated:
- logger.info(f'Disabled Remnawave user {user.remnawave_uuid} for subscription reset')
+ logger.info('Disabled Remnawave user for subscription reset', remnawave_uuid=user.remnawave_uuid)
except Exception as e:
panel_error = str(e)
- logger.warning(f'Failed to disable Remnawave user during subscription reset: {e}')
+ logger.warning('Failed to disable Remnawave user during subscription reset', error=e)
# Delete subscription from database
from sqlalchemy import delete
@@ -1888,7 +1911,7 @@ async def reset_user_subscription(
await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else ''
- logger.info(f'Admin {admin.id} reset subscription for user {user_id}{reason_text}')
+ logger.info('Admin reset subscription for user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
return ResetSubscriptionResponse(
success=True,
@@ -1933,10 +1956,10 @@ async def disable_user(
subscription_service = SubscriptionService()
panel_deactivated = await subscription_service.disable_remnawave_user(user.remnawave_uuid)
if panel_deactivated:
- logger.info(f'Disabled Remnawave user {user.remnawave_uuid}')
+ logger.info('Disabled Remnawave user', remnawave_uuid=user.remnawave_uuid)
except Exception as e:
panel_error = str(e)
- logger.warning(f'Failed to disable Remnawave user: {e}')
+ logger.warning('Failed to disable Remnawave user', error=e)
# Deactivate subscription in bot database
if user.subscription:
@@ -1944,7 +1967,7 @@ async def disable_user(
await deactivate_subscription(db, user.subscription)
subscription_deactivated = True
- logger.info(f'Deactivated subscription for user {user_id}')
+ logger.info('Deactivated subscription for user', user_id=user_id)
# Block user account
user.status = UserStatus.BLOCKED.value
@@ -1952,7 +1975,7 @@ async def disable_user(
await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else ''
- logger.info(f'Admin {admin.id} disabled user {user_id}{reason_text}')
+ logger.info('Admin disabled user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
return DisableUserResponse(
success=True,
@@ -2199,7 +2222,7 @@ async def get_user_sync_status(
differences.append(f'Squads mismatch ({", ".join(squad_diff_parts)})')
except Exception as e:
- logger.warning(f'Failed to get panel data for user {user_id}: {e}')
+ logger.warning('Failed to get panel data for user', user_id=user_id, error=e)
differences.append(f'Error fetching panel data: {e!s}')
return PanelSyncStatusResponse(
@@ -2409,7 +2432,9 @@ async def sync_user_from_panel(
await db.commit()
- logger.info(f'Admin {admin.id} synced user {user_id} from panel. Changes: {list(changes.keys())}')
+ logger.info(
+ 'Admin synced user from panel. Changes', admin_id=admin.id, user_id=user_id, value=list(changes.keys())
+ )
return SyncFromPanelResponse(
success=True,
@@ -2422,7 +2447,7 @@ async def sync_user_from_panel(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Error syncing user {user_id} from panel: {e}')
+ logger.error('Error syncing user from panel', user_id=user_id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Sync error: {e!s}',
@@ -2510,7 +2535,7 @@ async def sync_user_to_panel(
if panel_uuid:
existing_user = await api.get_user_by_uuid(panel_uuid)
if not existing_user:
- logger.warning(f'User {user.id} has stale remnawave_uuid {panel_uuid}, clearing')
+ logger.warning('User has stale remnawave_uuid clearing', user_id=user.id, panel_uuid=panel_uuid)
panel_uuid = None
user.remnawave_uuid = None
@@ -2600,7 +2625,7 @@ async def sync_user_to_panel(
await db.commit()
- logger.info(f'Admin {admin.id} synced user {user_id} to panel. Action: {action}')
+ logger.info('Admin synced user to panel. Action', admin_id=admin.id, user_id=user_id, action=action)
return SyncToPanelResponse(
success=True,
@@ -2614,7 +2639,7 @@ async def sync_user_to_panel(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Error syncing user {user_id} to panel: {e}')
+ logger.error('Error syncing user to panel', user_id=user_id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Sync error: {e!s}',
diff --git a/app/cabinet/routes/admin_wheel.py b/app/cabinet/routes/admin_wheel.py
index e137a54f..bbef73ec 100644
--- a/app/cabinet/routes/admin_wheel.py
+++ b/app/cabinet/routes/admin_wheel.py
@@ -2,10 +2,10 @@
API роуты колеса удачи для администраторов.
"""
-import logging
import math
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
@@ -35,7 +35,7 @@ from app.database.models import User
from app.services.wheel_service import wheel_service
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/admin/wheel', tags=['Admin Fortune Wheel'])
@@ -107,7 +107,7 @@ async def update_admin_wheel_config(
config = await update_wheel_config(db, **update_data)
- logger.info(f'🎡 Admin {admin.telegram_id} updated wheel config: {update_data}')
+ logger.info('🎡 Admin updated wheel config', telegram_id=admin.telegram_id, update_data=update_data)
# Возвращаем полную конфигурацию
prizes = await get_wheel_prizes(db, config.id, active_only=False)
@@ -211,7 +211,7 @@ async def create_prize(
promo_traffic_gb=request.promo_traffic_gb,
)
- logger.info(f'🎁 Admin {admin.telegram_id} created prize: {prize.display_name}')
+ logger.info('🎁 Admin created prize', telegram_id=admin.telegram_id, display_name=prize.display_name)
return WheelPrizeAdminResponse(
id=prize.id,
@@ -261,7 +261,7 @@ async def update_prize(
detail='Prize not found',
)
- logger.info(f'🎁 Admin {admin.telegram_id} updated prize {prize_id}: {update_data}')
+ logger.info('🎁 Admin updated prize', telegram_id=admin.telegram_id, prize_id=prize_id, update_data=update_data)
return WheelPrizeAdminResponse(
id=prize.id,
@@ -298,7 +298,7 @@ async def delete_prize_endpoint(
detail='Prize not found',
)
- logger.info(f'🗑️ Admin {admin.telegram_id} deleted prize {prize_id}')
+ logger.info('🗑️ Admin deleted prize', telegram_id=admin.telegram_id, prize_id=prize_id)
@router.post('/prizes/reorder', status_code=status.HTTP_200_OK)
@@ -309,7 +309,7 @@ async def reorder_prizes(
):
"""Переупорядочить призы."""
await reorder_wheel_prizes(db, request.prize_ids)
- logger.info(f'🔄 Admin {admin.telegram_id} reordered prizes: {request.prize_ids}')
+ logger.info('🔄 Admin reordered prizes', telegram_id=admin.telegram_id, prize_ids=request.prize_ids)
return {'success': True}
diff --git a/app/cabinet/routes/auth.py b/app/cabinet/routes/auth.py
index db84eb1d..e09169fb 100644
--- a/app/cabinet/routes/auth.py
+++ b/app/cabinet/routes/auth.py
@@ -2,9 +2,9 @@
import asyncio
import hashlib
-import logging
from datetime import UTC, datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
@@ -69,7 +69,7 @@ from ..services.email_service import email_service
from ..services.email_template_overrides import get_rendered_override
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/auth', tags=['Cabinet Auth'])
@@ -158,12 +158,12 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
panel_users = await api.get_user_by_email(user.email)
if not panel_users:
- logger.debug(f'No subscription found in panel for email: {user.email}')
+ logger.debug('No subscription found in panel for email', email=user.email)
return
# Take first user if multiple found
panel_user = panel_users[0]
- logger.info(f'Found subscription in panel for email {user.email}: {panel_user.uuid}')
+ logger.info('Found subscription in panel for email', email=user.email, uuid=panel_user.uuid)
# Link user to panel
user.remnawave_uuid = panel_user.uuid
@@ -208,7 +208,10 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
existing_sub.device_limit = device_limit
existing_sub.is_trial = False # Panel subscription is not trial
logger.info(
- f'Updated subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}'
+ 'Updated subscription for email user squads: devices',
+ email=user.email,
+ connected_squads=connected_squads,
+ device_limit=device_limit,
)
else:
# Create new subscription (expire_at and current_time already naive UTC)
@@ -228,13 +231,16 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
)
db.add(new_sub)
logger.info(
- f'Created subscription for email user {user.email}, squads: {connected_squads}, devices: {device_limit}'
+ 'Created subscription for email user squads: devices',
+ email=user.email,
+ connected_squads=connected_squads,
+ device_limit=device_limit,
)
await db.commit()
except Exception as e:
- logger.warning(f'Failed to sync subscription from panel for {user.email}: {e}')
+ logger.warning('Failed to sync subscription from panel for', email=user.email, error=e)
# Don't rollback - it detaches user object and breaks subsequent operations
# The sync is non-critical, main verification already succeeded
@@ -275,7 +281,7 @@ async def auth_telegram(
if not user:
# Create new user from Telegram initData
- logger.info(f'Creating new user from cabinet (initData): telegram_id={telegram_id}')
+ logger.info('Creating new user from cabinet (initData): telegram_id', telegram_id=telegram_id)
user = await create_user(
db=db,
telegram_id=telegram_id,
@@ -284,7 +290,7 @@ async def auth_telegram(
last_name=tg_last_name,
language=tg_language,
)
- logger.info(f'User created successfully: id={user.id}, telegram_id={user.telegram_id}')
+ logger.info('User created successfully: id=, telegram_id', user_id=user.id, telegram_id=user.telegram_id)
else:
# Update user info from initData (like bot middleware does)
updated = False
@@ -298,7 +304,7 @@ async def auth_telegram(
user.last_name = tg_last_name
updated = True
if updated:
- logger.info(f'User {user.id} profile updated from initData')
+ logger.info('User profile updated from initData', user_id=user.id)
if user.status != 'active':
raise HTTPException(
@@ -341,7 +347,9 @@ async def auth_telegram_widget(
if not user:
# Create new user from Telegram data
- logger.info(f'Creating new user from cabinet: telegram_id={request.id}, username={request.username}')
+ logger.info(
+ 'Creating new user from cabinet: telegram_id=, username', request_id=request.id, username=request.username
+ )
user = await create_user(
db=db,
telegram_id=request.id,
@@ -350,7 +358,7 @@ async def auth_telegram_widget(
last_name=request.last_name,
language='ru',
)
- logger.info(f'User created successfully: id={user.id}, telegram_id={user.telegram_id}')
+ logger.info('User created successfully: id=, telegram_id', user_id=user.id, telegram_id=user.telegram_id)
if user.status != 'active':
raise HTTPException(
@@ -485,7 +493,7 @@ async def register_email_standalone(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Invalid test email password',
)
- logger.info(f'Test email registration: {request.email}')
+ logger.info('Test email registration', email=request.email)
# Check for disposable email
if disposable_email_service.is_disposable(request.email):
@@ -512,11 +520,17 @@ async def register_email_standalone(
if referrer:
# Защита от самореферала - нельзя регистрироваться по своему же коду
if referrer.email and referrer.email.lower() == request.email.lower():
- logger.warning(f'Self-referral attempt blocked: email={request.email}, code={request.referral_code}')
+ logger.warning(
+ 'Self-referral attempt blocked: email=, code',
+ email=request.email,
+ referral_code=request.referral_code,
+ )
referrer = None
else:
logger.info(
- f'Found referrer for email registration: referrer_id={referrer.id}, code={request.referral_code}'
+ 'Found referrer for email registration: referrer_id=, code',
+ referrer_id=referrer.id,
+ referral_code=request.referral_code,
)
# Создать пользователя
@@ -534,7 +548,7 @@ async def register_email_standalone(
user.email_verified = True
user.email_verified_at = datetime.utcnow()
await db.commit()
- logger.info(f'Test email auto-verified: {request.email}, user_id={user.id}')
+ logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id)
else:
# Сгенерировать токен верификации
verification_token = generate_verification_token()
@@ -579,9 +593,11 @@ async def register_email_standalone(
if referrer:
try:
await process_referral_registration(db, user.id, referrer.id, bot=None)
- logger.info(f'Processed referral registration: user_id={user.id}, referrer_id={referrer.id}')
+ logger.info(
+ 'Processed referral registration: user_id=, referrer_id', user_id=user.id, referrer_id=referrer.id
+ )
except Exception as e:
- logger.error(f'Failed to process referral registration: {e}')
+ logger.error('Failed to process referral registration', error=e)
# Не прерываем регистрацию из-за ошибки реферальной системы
# Для тестового email - сразу можно логиниться (уже verified)
@@ -724,7 +740,7 @@ async def login_email(
if not user:
# For test email - auto-create user if not exists
if is_test_email and settings.validate_test_email_password(request.email, request.password):
- logger.info(f'Test email login - creating new user: {request.email}')
+ logger.info('Test email login creating new user', email=request.email)
password_hash = hash_password(request.password)
user = await create_user_by_email(
db=db,
@@ -1051,9 +1067,16 @@ async def request_email_change(
custom_body_html=custom_body,
)
except Exception as e:
- logger.error(f'Failed to send verification email to {request.new_email} for user {user.id}: {e}')
+ logger.error(
+ 'Failed to send verification email to for user',
+ new_email=request.new_email,
+ user_id=user.id,
+ error=e,
+ )
- logger.info(f'Unverified email replaced for user {user.id}: {old_email} -> {request.new_email}')
+ logger.info(
+ 'Unverified email replaced for user', user_id=user.id, old_email=old_email, new_email=request.new_email
+ )
return EmailChangeResponse(
message='Email replaced, verification sent to new address',
@@ -1104,7 +1127,7 @@ async def request_email_change(
detail='Email service is not configured',
)
- logger.info(f'Email change requested for user {user.id}: {user.email} -> {request.new_email}')
+ logger.info('Email change requested for user', user_id=user.id, email=user.email, new_email=request.new_email)
return EmailChangeResponse(
message='Verification code sent to new email',
diff --git a/app/cabinet/routes/balance.py b/app/cabinet/routes/balance.py
index 4c109ceb..19d248b8 100644
--- a/app/cabinet/routes/balance.py
+++ b/app/cabinet/routes/balance.py
@@ -1,11 +1,11 @@
"""Balance and payment routes for cabinet."""
-import logging
import math
import time
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
import httpx
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -41,7 +41,7 @@ from ..schemas.balance import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/balance', tags=['Cabinet Balance'])
@@ -249,7 +249,7 @@ async def create_stars_invoice(
if stars_amount <= 0:
stars_amount = 1
except Exception as e:
- logger.error(f'Error calculating Stars amount: {e}')
+ logger.error('Error calculating Stars amount', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to calculate Stars amount',
@@ -279,7 +279,7 @@ async def create_stars_invoice(
result = response.json()
if not result.get('ok'):
- logger.error(f'Telegram API error: {result}')
+ logger.error('Telegram API error', result=result)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to create Stars invoice',
@@ -287,8 +287,10 @@ async def create_stars_invoice(
invoice_url = result['result']
logger.info(
- f'Created Stars invoice for balance top-up: user={user.id}, '
- f'amount={request.amount_kopeks} kopeks, stars={stars_amount}'
+ 'Created Stars invoice for balance top-up: user=, amount= kopeks, stars',
+ user_id=user.id,
+ amount_kopeks=request.amount_kopeks,
+ stars_amount=stars_amount,
)
return StarsInvoiceResponse(
@@ -298,7 +300,7 @@ async def create_stars_invoice(
)
except httpx.HTTPError as e:
- logger.error(f'HTTP error creating Stars invoice: {e}')
+ logger.error('HTTP error creating Stars invoice', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to connect to Telegram API',
@@ -710,7 +712,7 @@ async def create_topup(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Payment creation error: {e}')
+ logger.error('Payment creation error', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to create payment. Please try again later.',
diff --git a/app/cabinet/routes/branding.py b/app/cabinet/routes/branding.py
index 21fd9333..c36875a4 100644
--- a/app/cabinet/routes/branding.py
+++ b/app/cabinet/routes/branding.py
@@ -1,10 +1,10 @@
"""Branding routes for cabinet - logo, project name, and theme colors management."""
import json
-import logging
import os
from pathlib import Path
+import structlog
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from pydantic import BaseModel
@@ -17,7 +17,7 @@ from app.database.models import SystemSetting, User
from ..dependencies import get_cabinet_db, get_current_admin_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/branding', tags=['Branding'])
@@ -307,7 +307,7 @@ async def update_branding_name(
await set_setting_value(db, BRANDING_NAME_KEY, name)
- logger.info(f'Admin {admin.telegram_id} updated branding name to: {name}')
+ logger.info('Admin updated branding name to', telegram_id=admin.telegram_id, name=name)
# Return updated branding
custom_logo = has_custom_logo()
@@ -368,7 +368,7 @@ async def upload_logo(
# Mark that we have a custom logo
await set_setting_value(db, BRANDING_LOGO_KEY, 'custom')
- logger.info(f'Admin {admin.telegram_id} uploaded new logo: {logo_path}')
+ logger.info('Admin uploaded new logo', telegram_id=admin.telegram_id, logo_path=logo_path)
# Get current name for response
name = await get_setting_value(db, BRANDING_NAME_KEY)
@@ -398,7 +398,7 @@ async def delete_logo(
# Update setting
await set_setting_value(db, BRANDING_LOGO_KEY, 'default')
- logger.info(f'Admin {admin.telegram_id} deleted custom logo')
+ logger.info('Admin deleted custom logo', telegram_id=admin.telegram_id)
# Get current name for response
name = await get_setting_value(db, BRANDING_NAME_KEY)
@@ -486,7 +486,7 @@ async def update_theme_colors(
# Save to database
await set_setting_value(db, THEME_COLORS_KEY, json.dumps(current_colors))
- logger.info(f'Admin {admin.telegram_id} updated theme colors: {list(update_data.keys())}')
+ logger.info('Admin updated theme colors', telegram_id=admin.telegram_id, value=list(update_data.keys()))
return ThemeColorsResponse(**current_colors)
@@ -500,7 +500,7 @@ async def reset_theme_colors(
# Save default colors
await set_setting_value(db, THEME_COLORS_KEY, json.dumps(DEFAULT_THEME_COLORS))
- logger.info(f'Admin {admin.telegram_id} reset theme colors to defaults')
+ logger.info('Admin reset theme colors to defaults', telegram_id=admin.telegram_id)
return ThemeColorsResponse(**DEFAULT_THEME_COLORS)
@@ -558,7 +558,7 @@ async def update_enabled_themes(
# Save to database
await set_setting_value(db, ENABLED_THEMES_KEY, json.dumps(current_themes))
- logger.info(f'Admin {admin.telegram_id} updated enabled themes: {current_themes}')
+ logger.info('Admin updated enabled themes', telegram_id=admin.telegram_id, current_themes=current_themes)
return EnabledThemesResponse(**current_themes)
@@ -593,7 +593,7 @@ async def update_animation_enabled(
"""Update animation enabled setting. Admin only."""
await set_setting_value(db, ANIMATION_ENABLED_KEY, str(payload.enabled).lower())
- logger.info(f'Admin {admin.telegram_id} set animation enabled: {payload.enabled}')
+ logger.info('Admin set animation enabled', telegram_id=admin.telegram_id, enabled=payload.enabled)
return AnimationEnabledResponse(enabled=payload.enabled)
@@ -628,7 +628,7 @@ async def update_fullscreen_enabled(
"""Update fullscreen enabled setting. Admin only."""
await set_setting_value(db, FULLSCREEN_ENABLED_KEY, str(payload.enabled).lower())
- logger.info(f'Admin {admin.telegram_id} set fullscreen enabled: {payload.enabled}')
+ logger.info('Admin set fullscreen enabled', telegram_id=admin.telegram_id, enabled=payload.enabled)
return FullscreenEnabledResponse(enabled=payload.enabled)
@@ -664,7 +664,7 @@ async def update_email_auth_enabled(
"""Update email auth enabled setting. Admin only."""
await set_setting_value(db, EMAIL_AUTH_ENABLED_KEY, str(payload.enabled).lower())
- logger.info(f'Admin {admin.telegram_id} set email auth enabled: {payload.enabled}')
+ logger.info('Admin set email auth enabled', telegram_id=admin.telegram_id, enabled=payload.enabled)
return EmailAuthEnabledResponse(enabled=payload.enabled)
@@ -719,7 +719,7 @@ async def update_analytics_counters(
if payload.google_ads_label is not None:
await set_setting_value(db, GOOGLE_ADS_LABEL_KEY, payload.google_ads_label.strip())
- logger.info(f'Admin {admin.telegram_id} updated analytics counters')
+ logger.info('Admin updated analytics counters', telegram_id=admin.telegram_id)
# Return current state
yandex_id = await get_setting_value(db, YANDEX_METRIKA_ID_KEY) or ''
@@ -764,6 +764,6 @@ async def update_lite_mode_enabled(
"""Update lite mode enabled setting. Admin only."""
await set_setting_value(db, LITE_MODE_ENABLED_KEY, str(payload.enabled).lower())
- logger.info(f'Admin {admin.telegram_id} set lite mode enabled: {payload.enabled}')
+ logger.info('Admin set lite mode enabled', telegram_id=admin.telegram_id, enabled=payload.enabled)
return LiteModeEnabledResponse(enabled=payload.enabled)
diff --git a/app/cabinet/routes/contests.py b/app/cabinet/routes/contests.py
index f7f865ef..1600e555 100644
--- a/app/cabinet/routes/contests.py
+++ b/app/cabinet/routes/contests.py
@@ -1,10 +1,10 @@
"""Contests routes for cabinet - user participation in games/contests."""
-import logging
import random
from datetime import datetime, timedelta
from typing import Any
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
@@ -30,7 +30,7 @@ from app.services.contest_rotation_service import (
from ..dependencies import get_cabinet_db, get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/contests', tags=['Cabinet Contests'])
@@ -106,7 +106,7 @@ async def _award_prize(db: AsyncSession, user_id: int, prize_type: str, prize_va
await db.commit()
await db.refresh(subscription)
- logger.info(f'🎁 Extended subscription for user {user_id} by {days} days (contest prize)')
+ logger.info('🎁 Extended subscription for user by days (contest prize)', user_id=user_id, days=days)
return f'Subscription extended by {days} days'
if prize_type == 'balance':
@@ -125,10 +125,10 @@ async def _award_prize(db: AsyncSession, user_id: int, prize_type: str, prize_va
await db.commit()
await db.refresh(user)
- logger.info(f'🎁 Added {amount} to balance for user {user_id} (contest prize)')
+ logger.info('🎁 Added to balance for user (contest prize)', amount=amount, user_id=user_id)
return f'Balance increased by {amount}'
- logger.warning(f'Unknown prize type: {prize_type}')
+ logger.warning('Unknown prize type', prize_type=prize_type)
return f"Prize type '{prize_type}' not supported"
diff --git a/app/cabinet/routes/info.py b/app/cabinet/routes/info.py
index 16b9b037..68b89cac 100644
--- a/app/cabinet/routes/info.py
+++ b/app/cabinet/routes/info.py
@@ -1,7 +1,6 @@
"""Info pages routes for cabinet - FAQ, rules, privacy policy, etc."""
-import logging
-
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +15,7 @@ from app.services.public_offer_service import PublicOfferService
from ..dependencies import get_cabinet_db, get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/info', tags=['Cabinet Info'])
diff --git a/app/cabinet/routes/media.py b/app/cabinet/routes/media.py
index ed137992..4cc99427 100644
--- a/app/cabinet/routes/media.py
+++ b/app/cabinet/routes/media.py
@@ -1,8 +1,8 @@
"""Media upload/download routes for cabinet tickets."""
-import logging
import mimetypes
+import structlog
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
@@ -16,7 +16,7 @@ from app.database.models import User
from ..dependencies import get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/media', tags=['Cabinet Media'])
@@ -125,7 +125,12 @@ async def upload_media(
media_url = _build_media_url(request, media.file_id)
- logger.info(f'User {user.telegram_id} uploaded {media_type_normalized}: {media.file_id}')
+ logger.info(
+ 'User uploaded',
+ telegram_id=user.telegram_id,
+ media_type_normalized=media_type_normalized,
+ file_id=media.file_id,
+ )
return MediaUploadResponse(
media_type=media_type_normalized,
@@ -136,7 +141,7 @@ async def upload_media(
except HTTPException:
raise
except Exception as error:
- logger.error(f'Failed to upload media for user {user.telegram_id}: {error}')
+ logger.error('Failed to upload media for user', telegram_id=user.telegram_id, error=error)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to upload media',
@@ -187,7 +192,7 @@ async def download_media(
except HTTPException:
raise
except Exception as error:
- logger.error(f'Failed to download media {file_id}: {error}')
+ logger.error('Failed to download media', file_id=file_id, error=error)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to download media',
diff --git a/app/cabinet/routes/notifications.py b/app/cabinet/routes/notifications.py
index c2d4bdd6..6da075a0 100644
--- a/app/cabinet/routes/notifications.py
+++ b/app/cabinet/routes/notifications.py
@@ -1,9 +1,9 @@
"""Notification settings routes for cabinet."""
-import logging
from datetime import datetime
from typing import Any
+import structlog
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +13,7 @@ from app.database.models import User
from ..dependencies import get_cabinet_db, get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/notifications', tags=['Cabinet Notifications'])
diff --git a/app/cabinet/routes/oauth.py b/app/cabinet/routes/oauth.py
index 049dc8d7..46f3e1c0 100644
--- a/app/cabinet/routes/oauth.py
+++ b/app/cabinet/routes/oauth.py
@@ -1,8 +1,8 @@
"""OAuth 2.0 authentication routes for cabinet."""
-import logging
from datetime import UTC, datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
@@ -27,7 +27,7 @@ from ..schemas.auth import AuthResponse
from .auth import _create_auth_response, _store_refresh_token
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth'])
@@ -120,7 +120,7 @@ async def oauth_callback(
try:
token_data = await oauth_provider.exchange_code(request.code)
except Exception as exc:
- logger.error('OAuth code exchange failed for %s: %s', provider, exc)
+ logger.error('OAuth code exchange failed for', provider=provider, exc=exc)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Failed to exchange authorization code',
@@ -130,7 +130,7 @@ async def oauth_callback(
try:
user_info: OAuthUserInfo = await oauth_provider.get_user_info(token_data)
except Exception as exc:
- logger.error('OAuth user info fetch failed for %s: %s', provider, exc)
+ logger.error('OAuth user info fetch failed for', provider=provider, exc=exc)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Failed to fetch user information from provider',
@@ -139,7 +139,7 @@ async def oauth_callback(
# 5. Find user by provider ID
user = await get_user_by_oauth_provider(db, provider, user_info.provider_id)
if user:
- logger.info('OAuth login via %s for existing user %s', provider, user.id)
+ logger.info('OAuth login via for existing user', provider=provider, user_id=user.id)
return await _finalize_oauth_login(db, user, provider)
# 6. Find user by email (if verified) and link provider
@@ -147,7 +147,7 @@ async def oauth_callback(
user = await get_user_by_email(db, user_info.email)
if user:
await set_user_oauth_provider_id(db, user, provider, user_info.provider_id)
- logger.info('OAuth login via %s linked to existing email user %s', provider, user.id)
+ logger.info('OAuth login via linked to existing email user', provider=provider, user_id=user.id)
return await _finalize_oauth_login(db, user, provider)
# 7. Create new user
@@ -161,5 +161,5 @@ async def oauth_callback(
last_name=user_info.last_name,
username=user_info.username,
)
- logger.info('OAuth new user created via %s with id=%s', provider, user.id)
+ logger.info('OAuth new user created via with id', provider=provider, user_id=user.id)
return await _finalize_oauth_login(db, user, provider)
diff --git a/app/cabinet/routes/polls.py b/app/cabinet/routes/polls.py
index 1585ef61..c66e9ae3 100644
--- a/app/cabinet/routes/polls.py
+++ b/app/cabinet/routes/polls.py
@@ -1,8 +1,8 @@
"""Polls routes for cabinet - user participation in polls/surveys."""
-import logging
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
@@ -20,7 +20,7 @@ from app.services.poll_service import get_next_question, get_question_option, re
from ..dependencies import get_cabinet_db, get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/polls', tags=['Cabinet Polls'])
diff --git a/app/cabinet/routes/promo.py b/app/cabinet/routes/promo.py
index df213b53..d3ca3994 100644
--- a/app/cabinet/routes/promo.py
+++ b/app/cabinet/routes/promo.py
@@ -1,9 +1,9 @@
"""Promo offers routes for cabinet - personal discounts and offers."""
-import logging
from datetime import datetime, timedelta
from typing import Any
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import and_, select
@@ -22,7 +22,7 @@ from app.services.promo_offer_service import promo_offer_service
from ..dependencies import get_cabinet_db, get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/promo', tags=['Cabinet Promo'])
diff --git a/app/cabinet/routes/promocode.py b/app/cabinet/routes/promocode.py
index 5b6a5ab0..3d83730e 100644
--- a/app/cabinet/routes/promocode.py
+++ b/app/cabinet/routes/promocode.py
@@ -1,7 +1,6 @@
"""Promo code routes for cabinet."""
-import logging
-
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,7 +11,7 @@ from app.services.promocode_service import PromoCodeService
from ..dependencies import get_cabinet_db, get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/promocode', tags=['Cabinet Promocode'])
diff --git a/app/cabinet/routes/referral.py b/app/cabinet/routes/referral.py
index 43ef4c4e..277cc675 100644
--- a/app/cabinet/routes/referral.py
+++ b/app/cabinet/routes/referral.py
@@ -1,8 +1,8 @@
"""Referral program routes for cabinet."""
-import logging
import math
+import structlog
from fastapi import APIRouter, Depends, Query
from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,7 +22,7 @@ from ..schemas.referral import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/referral', tags=['Cabinet Referral'])
diff --git a/app/cabinet/routes/subscription.py b/app/cabinet/routes/subscription.py
index 8f879b10..7655fa79 100644
--- a/app/cabinet/routes/subscription.py
+++ b/app/cabinet/routes/subscription.py
@@ -2,11 +2,11 @@
import base64
import json
-import logging
import re
from datetime import UTC, datetime, timedelta
from typing import Any
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -58,7 +58,7 @@ from ..schemas.subscription import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/subscription', tags=['Cabinet Subscription'])
@@ -490,9 +490,9 @@ async def renew_subscription(
try:
await user_cart_service.save_user_cart(user.id, cart_data)
- logger.info(f'Cart saved for auto-renewal (cabinet) user {user.id}')
+ logger.info('Cart saved for auto-renewal (cabinet) user', user_id=user.id)
except Exception as e:
- logger.error(f'Error saving cart for auto-renewal (cabinet): {e}')
+ logger.error('Error saving cart for auto-renewal (cabinet)', error=e)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -550,7 +550,7 @@ async def renew_subscription(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send admin notification for subscription renewal: {e}')
+ logger.error('Failed to send admin notification for subscription renewal', error=e)
response = {
'message': 'Subscription renewed successfully',
@@ -789,11 +789,13 @@ async def purchase_traffic(
try:
await user_cart_service.save_user_cart(user.id, cart_data)
logger.info(
- f'Cart saved for traffic purchase (cabinet) user {user.id}: '
- f'+{request.gb} GB, discount {traffic_discount_percent}%'
+ 'Cart saved for traffic purchase (cabinet) user + discount',
+ user_id=user.id,
+ gb=request.gb,
+ traffic_discount_percent=traffic_discount_percent,
)
except Exception as e:
- logger.error(f'Error saving cart for traffic purchase (cabinet): {e}')
+ logger.error('Error saving cart for traffic purchase (cabinet)', error=e)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -833,7 +835,11 @@ async def purchase_traffic(
from datetime import timedelta
subscription.traffic_reset_at = datetime.utcnow() + timedelta(days=30)
- logger.info(f'Set traffic_reset_at for subscription {subscription.id}: {subscription.traffic_reset_at}')
+ logger.info(
+ 'Set traffic_reset_at for subscription',
+ subscription_id=subscription.id,
+ traffic_reset_at=subscription.traffic_reset_at,
+ )
await db.commit()
@@ -845,7 +851,7 @@ async def purchase_traffic(
else:
await subscription_service.create_remnawave_user(db, subscription)
except Exception as e:
- logger.error(f'Failed to sync traffic with RemnaWave: {e}')
+ logger.error('Failed to sync traffic with RemnaWave', error=e)
# Создаём транзакцию
await create_transaction(
@@ -882,7 +888,7 @@ async def purchase_traffic(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send admin notification for traffic purchase: {e}')
+ logger.error('Failed to send admin notification for traffic purchase', error=e)
response = {
'success': True,
@@ -946,9 +952,13 @@ async def purchase_devices_legacy(
'source': 'cabinet',
}
await user_cart_service.save_user_cart(user.id, cart_data)
- logger.info(f'Cart saved for device purchase (cabinet /devices) user {user.id}: +{request.devices} devices')
+ logger.info(
+ 'Cart saved for device purchase (cabinet /devices) user + devices',
+ user_id=user.id,
+ devices=request.devices,
+ )
except Exception as e:
- logger.error(f'Error saving cart for device purchase (cabinet /devices): {e}')
+ logger.error('Error saving cart for device purchase (cabinet /devices)', error=e)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -1020,7 +1030,7 @@ async def purchase_devices_legacy(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send admin notification for device purchase: {e}')
+ logger.error('Failed to send admin notification for device purchase', error=e)
response = {
'message': 'Devices added successfully',
@@ -1123,7 +1133,7 @@ async def get_trial_info(
if tariff_trial_days:
duration_days = tariff_trial_days
except Exception as e:
- logger.error(f'Error getting trial tariff for info: {e}')
+ logger.error('Error getting trial tariff for info', error=e)
# Check if user already has an active subscription
if user.subscription:
@@ -1211,7 +1221,7 @@ async def activate_trial(
detail=f'Insufficient balance. Need {price_kopeks / 100:.2f} RUB',
)
user.balance_kopeks -= price_kopeks
- logger.info(f'User {user.id} paid {price_kopeks} kopeks for trial activation')
+ logger.info('User paid kopeks for trial activation', user_id=user.id, price_kopeks=price_kopeks)
# Get trial parameters from tariff if configured (same logic as bot handler)
trial_duration = settings.TRIAL_DURATION_DAYS
@@ -1243,9 +1253,14 @@ async def activate_trial(
tariff_trial_days = getattr(trial_tariff, 'trial_duration_days', None)
if tariff_trial_days:
trial_duration = tariff_trial_days
- logger.info(f'Using trial tariff {trial_tariff.name} (ID: {trial_tariff.id}) with squads: {trial_squads}')
+ logger.info(
+ 'Using trial tariff (ID: ) with squads',
+ trial_tariff_name=trial_tariff.name,
+ trial_tariff_id=trial_tariff.id,
+ trial_squads=trial_squads,
+ )
except Exception as e:
- logger.error(f'Error getting trial tariff: {e}')
+ logger.error('Error getting trial tariff', error=e)
# Create trial subscription
subscription = await create_trial_subscription(
@@ -1258,7 +1273,7 @@ async def activate_trial(
tariff_id=tariff_id_for_trial,
)
- logger.info(f'Trial subscription activated for user {user.id}')
+ logger.info('Trial subscription activated for user', user_id=user.id)
# Create RemnaWave user
try:
@@ -1267,7 +1282,7 @@ async def activate_trial(
await subscription_service.create_remnawave_user(db, subscription)
await db.refresh(subscription)
except Exception as e:
- logger.error(f'Failed to create RemnaWave user for trial: {e}')
+ logger.error('Failed to create RemnaWave user for trial', error=e)
# Send admin notification about trial activation
try:
@@ -1286,7 +1301,7 @@ async def activate_trial(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send trial activation notification: {e}')
+ logger.error('Failed to send trial activation notification', error=e)
return _subscription_to_response(subscription)
@@ -1551,7 +1566,7 @@ async def get_purchase_options(
detail=str(e),
)
except Exception as e:
- logger.error(f'Failed to build purchase options for user {user.id}: {e}')
+ logger.error('Failed to build purchase options for user', user_id=user.id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to load purchase options',
@@ -1596,7 +1611,7 @@ async def preview_purchase(
detail=str(e),
)
except Exception as e:
- logger.error(f'Failed to calculate purchase preview for user {user.id}: {e}')
+ logger.error('Failed to calculate purchase preview for user', user_id=user.id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to calculate price',
@@ -1658,7 +1673,7 @@ async def submit_purchase(
bot=None,
)
except Exception as notif_error:
- logger.warning(f'Failed to send subscription notification to {user.email}: {notif_error}')
+ logger.warning('Failed to send subscription notification to', email=user.email, notif_error=notif_error)
# Отправляем уведомление админам о покупке подписки
try:
@@ -1684,7 +1699,7 @@ async def submit_purchase(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send admin notification for subscription purchase: {e}')
+ logger.error('Failed to send admin notification for subscription purchase', error=e)
return {
'success': True,
@@ -1716,9 +1731,9 @@ async def submit_purchase(
'source': 'cabinet',
}
await user_cart_service.save_user_cart(user.id, cart_data)
- logger.info(f'Cart saved for auto-purchase (cabinet /purchase) user {user.id}')
+ logger.info('Cart saved for auto-purchase (cabinet /purchase) user', user_id=user.id)
except Exception as cart_error:
- logger.error(f'Error saving cart for auto-purchase (cabinet /purchase): {cart_error}')
+ logger.error('Error saving cart for auto-purchase (cabinet /purchase)', cart_error=cart_error)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -1730,7 +1745,7 @@ async def submit_purchase(
},
)
except Exception as e:
- logger.error(f'Failed to submit purchase for user {user.id}: {e}')
+ logger.error('Failed to submit purchase for user', user_id=user.id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to process purchase',
@@ -1912,9 +1927,9 @@ async def purchase_tariff(
try:
await user_cart_service.save_user_cart(user.id, cart_data)
- logger.info(f'Cart saved for auto-purchase (cabinet) user {user.id}, tariff {tariff.id}')
+ logger.info('Cart saved for auto-purchase (cabinet) user tariff', user_id=user.id, tariff_id=tariff.id)
except Exception as e:
- logger.error(f'Error saving cart for auto-purchase (cabinet): {e}')
+ logger.error('Error saving cart for auto-purchase (cabinet)', error=e)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -2019,7 +2034,7 @@ async def purchase_tariff(
reset_reason='покупка тарифа (cabinet)',
)
except Exception as remnawave_error:
- logger.error(f'Failed to sync subscription with RemnaWave: {remnawave_error}')
+ logger.error('Failed to sync subscription with RemnaWave', remnawave_error=remnawave_error)
# Save cart for auto-renewal (not for daily tariffs - they have their own charging)
if not is_daily_tariff:
@@ -2033,9 +2048,9 @@ async def purchase_tariff(
'description': f'Продление тарифа {tariff.name} на {period_days} дней',
}
await user_cart_service.save_user_cart(user.id, cart_data)
- logger.info(f'Tariff cart saved for auto-renewal (cabinet) user {user.id}')
+ logger.info('Tariff cart saved for auto-renewal (cabinet) user', user_id=user.id)
except Exception as e:
- logger.error(f'Error saving tariff cart (cabinet): {e}')
+ logger.error('Error saving tariff cart (cabinet)', error=e)
await db.refresh(user)
@@ -2095,7 +2110,7 @@ async def purchase_tariff(
bot=None,
)
except Exception as notif_error:
- logger.warning(f'Failed to send subscription notification to {user.email}: {notif_error}')
+ logger.warning('Failed to send subscription notification to', email=user.email, notif_error=notif_error)
# Отправляем уведомление админам о покупке/продлении тарифа
try:
@@ -2124,14 +2139,14 @@ async def purchase_tariff(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send admin notification for tariff purchase: {e}')
+ logger.error('Failed to send admin notification for tariff purchase', error=e)
return response
except HTTPException:
raise
except Exception as e:
- logger.error(f'Failed to purchase tariff for user {user.id}: {e}')
+ logger.error('Failed to purchase tariff for user', user_id=user.id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to process tariff purchase',
@@ -2238,11 +2253,13 @@ async def purchase_devices(
}
await user_cart_service.save_user_cart(user.id, cart_data)
logger.info(
- f'Cart saved for device purchase (cabinet) user {user.id}: '
- f'+{request.devices} devices, discount {devices_discount_percent}%'
+ 'Cart saved for device purchase (cabinet) user + devices, discount',
+ user_id=user.id,
+ devices=request.devices,
+ devices_discount_percent=devices_discount_percent,
)
except Exception as e:
- logger.error(f'Error saving cart for device purchase (cabinet): {e}')
+ logger.error('Error saving cart for device purchase (cabinet)', error=e)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
@@ -2288,17 +2305,23 @@ async def purchase_devices(
else:
await service.create_remnawave_user(db, subscription)
except Exception as e:
- logger.error(f'Failed to sync devices with RemnaWave: {e}')
+ logger.error('Failed to sync devices with RemnaWave', error=e)
await db.refresh(user)
if devices_discount_percent > 0:
logger.info(
- f'User {user.id} purchased {request.devices} devices for {price_kopeks} kopeks '
- f'(discount {devices_discount_percent}%, saved {discount_value} kopeks)'
+ 'User purchased devices for kopeks (discount saved kopeks)',
+ user_id=user.id,
+ devices=request.devices,
+ price_kopeks=price_kopeks,
+ devices_discount_percent=devices_discount_percent,
+ discount_value=discount_value,
)
else:
- logger.info(f'User {user.id} purchased {request.devices} devices for {price_kopeks} kopeks')
+ logger.info(
+ 'User purchased devices for kopeks', user_id=user.id, devices=request.devices, price_kopeks=price_kopeks
+ )
# Отправляем уведомление админам
try:
@@ -2322,7 +2345,7 @@ async def purchase_devices(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send admin notification for device purchase: {e}')
+ logger.error('Failed to send admin notification for device purchase', error=e)
response = {
'success': True,
@@ -2345,7 +2368,7 @@ async def purchase_devices(
except HTTPException:
raise
except Exception as e:
- logger.error(f'Failed to purchase devices for user {user.id}: {e}')
+ logger.error('Failed to purchase devices for user', user_id=user.id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Не удалось обработать покупку устройств',
@@ -2463,7 +2486,7 @@ async def save_traffic_cart(
'description': f'Докупка {request.gb} ГБ трафика',
}
await user_cart_service.save_user_cart(user.id, cart_data)
- logger.info(f'Cart saved for traffic purchase (cabinet save-cart) user {user.id}: +{request.gb} GB')
+ logger.info('Cart saved for traffic purchase (cabinet save-cart) user +', user_id=user.id, gb=request.gb)
return {'success': True, 'cart_saved': True}
@@ -2538,7 +2561,9 @@ async def save_devices_cart(
'source': 'cabinet',
}
await user_cart_service.save_user_cart(user.id, cart_data)
- logger.info(f'Cart saved for device purchase (cabinet save-cart) user {user.id}: +{request.devices} devices')
+ logger.info(
+ 'Cart saved for device purchase (cabinet save-cart) user + devices', user_id=user.id, devices=request.devices
+ )
return {'success': True, 'cart_saved': True}
@@ -2664,7 +2689,7 @@ def _load_app_config_from_file() -> dict[str, Any]:
if isinstance(data, dict):
return data
except Exception as e:
- logger.error(f'Failed to load app-config.json: {e}')
+ logger.error('Failed to load app-config.json', error=e)
return {}
@@ -2807,10 +2832,11 @@ def _get_url_scheme_for_app(app: dict[str, Any]) -> tuple[str, bool]:
# No scheme found
logger.debug(
- f"_get_url_scheme_for_app: No scheme found for app '{app.get('name')}', "
- f'has blocks: {bool(app.get("blocks"))}, '
- f'has buttons: {bool(app.get("buttons"))}, '
- f'has urlScheme: {bool(app.get("urlScheme"))}'
+ '_get_url_scheme_for_app: No scheme found for app has blocks: has buttons: has urlScheme',
+ get=app.get('name'),
+ get_2=bool(app.get('blocks')),
+ get_3=bool(app.get('buttons')),
+ get_4=bool(app.get('urlScheme')),
)
return '', False
@@ -2856,7 +2882,7 @@ def _convert_remnawave_app_to_cabinet(app: dict[str, Any]) -> dict[str, Any]:
# Debug log for conversion (не логируем отсутствие urlScheme - для Happ это нормально)
app_name = app.get('name', 'unknown')
if url_scheme:
- logger.debug(f"_convert_remnawave_app_to_cabinet: app '{app_name}' -> urlScheme='{url_scheme}'")
+ logger.debug('_convert_remnawave_app_to_cabinet: app urlScheme', app_name=app_name, url_scheme=url_scheme)
# Smart block mapping: find blocks by their content, not just position
# 1. First block is usually installation
@@ -2965,12 +2991,12 @@ async def _load_app_config_async() -> dict[str, Any]:
async with service.get_api_client() as api:
config = await api.get_subscription_page_config(remnawave_uuid)
if config and config.config:
- logger.debug(f'Loaded app config from RemnaWave: {remnawave_uuid}')
+ logger.debug('Loaded app config from RemnaWave', remnawave_uuid=remnawave_uuid)
raw = dict(config.config)
raw['_isRemnawave'] = True
return raw
except Exception as e:
- logger.warning(f'Failed to load RemnaWave config, falling back to file: {e}')
+ logger.warning('Failed to load RemnaWave config, falling back to file', error=e)
# Fallback to local file
return _load_app_config_from_file()
@@ -3001,21 +3027,21 @@ def _create_deep_link(
scheme, uses_crypto = _get_url_scheme_for_app(app)
if not scheme:
- logger.debug(f"_create_deep_link: no urlScheme for app '{app.get('name', 'unknown')}'")
+ logger.debug('_create_deep_link: no urlScheme for app', get=app.get('name', 'unknown'))
return None
# Pick the correct payload based on which template the app uses
if uses_crypto:
if not subscription_crypto_link:
logger.debug(
- f"_create_deep_link: app '{app.get('name', 'unknown')}' requires crypto link but none available"
+ '_create_deep_link: app requires crypto link but none available', get=app.get('name', 'unknown')
)
return None
payload = subscription_crypto_link
else:
if not subscription_url:
logger.debug(
- f"_create_deep_link: app '{app.get('name', 'unknown')}' requires subscription_url but none available"
+ '_create_deep_link: app requires subscription_url but none available', get=app.get('name', 'unknown')
)
return None
payload = subscription_url
@@ -3024,7 +3050,7 @@ def _create_deep_link(
try:
payload = base64.b64encode(payload.encode('utf-8')).decode('utf-8')
except Exception as e:
- logger.warning(f'Failed to encode payload to base64: {e}')
+ logger.warning('Failed to encode payload to base64', error=e)
return f'{scheme}{payload}'
@@ -3233,7 +3259,7 @@ async def update_countries(
try:
await add_user_to_servers(db, added_server_ids)
except Exception as e:
- logger.error(f'Ошибка обновления счётчика серверов: {e}')
+ logger.error('Ошибка обновления счётчика серверов', error=e)
# Update connected squads
user.subscription.connected_squads = selected_countries
@@ -3248,7 +3274,7 @@ async def update_countries(
else:
await subscription_service.create_remnawave_user(db, user.subscription)
except Exception as e:
- logger.error(f'Failed to sync countries with RemnaWave: {e}')
+ logger.error('Failed to sync countries with RemnaWave', error=e)
await db.refresh(user.subscription)
@@ -3583,7 +3609,7 @@ async def get_devices(
}
except Exception as e:
- logger.error(f'Error fetching devices: {e}')
+ logger.error('Error fetching devices', error=e)
return {
'devices': [],
'total': 0,
@@ -3627,7 +3653,7 @@ async def delete_device(
}
except Exception as e:
- logger.error(f'Error deleting device: {e}')
+ logger.error('Error deleting device', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to delete device',
@@ -3686,7 +3712,7 @@ async def delete_all_devices(
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
deleted_count += 1
except Exception as device_error:
- logger.error(f'Error deleting device {device_hwid}: {device_error}')
+ logger.error('Error deleting device', device_hwid=device_hwid, device_error=device_error)
return {
'success': True,
@@ -3695,7 +3721,7 @@ async def delete_all_devices(
}
except Exception as e:
- logger.error(f'Error deleting all devices: {e}')
+ logger.error('Error deleting all devices', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to delete devices',
@@ -3769,7 +3795,7 @@ async def get_device_reduction_info(
if response and 'response' in response:
connected_devices_count = response['response'].get('total', 0)
except Exception as e:
- logger.error(f'Error getting connected devices count: {e}')
+ logger.error('Error getting connected devices count', error=e)
can_reduce = current_device_limit - min_device_limit
@@ -3853,8 +3879,11 @@ async def reduce_devices(
if connected_devices_count > new_device_limit:
devices_to_remove = connected_devices_count - new_device_limit
logger.info(
- f'Removing {devices_to_remove} excess devices for user {user.id}: '
- f'had {connected_devices_count}, new limit {new_device_limit}'
+ 'Removing excess devices for user had new limit',
+ devices_to_remove=devices_to_remove,
+ user_id=user.id,
+ connected_devices_count=connected_devices_count,
+ new_device_limit=new_device_limit,
)
# Sort by date (oldest first) and remove the last ones
@@ -3871,11 +3900,11 @@ async def reduce_devices(
delete_data = {'userUuid': user.remnawave_uuid, 'hwid': device_hwid}
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
devices_removed_count += 1
- logger.info(f'Removed device {device_hwid} for user {user.id}')
+ logger.info('Removed device for user', device_hwid=device_hwid, user_id=user.id)
except Exception as del_error:
- logger.error(f'Error removing device {device_hwid}: {del_error}')
+ logger.error('Error removing device', device_hwid=device_hwid, del_error=del_error)
except Exception as e:
- logger.error(f'Error checking/removing devices: {e}')
+ logger.error('Error checking/removing devices', error=e)
old_device_limit = current_device_limit
@@ -3889,7 +3918,7 @@ async def reduce_devices(
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
except Exception as e:
- logger.error(f'Error updating RemnaWave user: {e}')
+ logger.error('Error updating RemnaWave user', error=e)
logger.info(
f'User {user.id} reduced device limit from {old_device_limit} to {new_device_limit}'
@@ -4323,7 +4352,7 @@ async def switch_tariff(
else:
await subscription_service.create_remnawave_user(db, user.subscription)
except Exception as e:
- logger.error(f'Failed to sync tariff switch with RemnaWave: {e}')
+ logger.error('Failed to sync tariff switch with RemnaWave', error=e)
# Reset all devices on tariff switch
devices_reset = False
@@ -4333,9 +4362,9 @@ async def switch_tariff(
async with service.get_api_client() as api:
await api.reset_user_devices(user.remnawave_uuid)
devices_reset = True
- logger.info(f'Reset all devices for user {user.id} on tariff switch')
+ logger.info('Reset all devices for user on tariff switch', user_id=user.id)
except Exception as e:
- logger.error(f'Failed to reset devices on tariff switch: {e}')
+ logger.error('Failed to reset devices on tariff switch', error=e)
await db.refresh(user)
await db.refresh(user.subscription)
@@ -4363,7 +4392,7 @@ async def switch_tariff(
finally:
await bot.session.close()
except Exception as e:
- logger.error(f'Failed to send admin notification for tariff switch: {e}')
+ logger.error('Failed to send admin notification for tariff switch', error=e)
response = {
'success': True,
@@ -4462,7 +4491,7 @@ async def toggle_subscription_pause(
subscription_service = SubscriptionService()
await subscription_service.enable_remnawave_user(user.remnawave_uuid)
except Exception as e:
- logger.error(f'Error enabling RemnaWave user on resume: {e}')
+ logger.error('Error enabling RemnaWave user on resume', error=e)
if new_paused_state:
message = 'Daily subscription paused'
@@ -4596,7 +4625,7 @@ async def switch_traffic_package(
else:
await subscription_service.create_remnawave_user(db, user.subscription)
except Exception as e:
- logger.error(f'Failed to sync traffic switch with RemnaWave: {e}')
+ logger.error('Failed to sync traffic switch with RemnaWave', error=e)
await db.refresh(user)
await db.refresh(user.subscription)
@@ -4737,7 +4766,7 @@ async def refresh_traffic(
}
except Exception as e:
- logger.error(f'Error refreshing traffic for user {user.id}: {e}')
+ logger.error('Error refreshing traffic for user', user_id=user.id, error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to refresh traffic data',
diff --git a/app/cabinet/routes/ticket_notifications.py b/app/cabinet/routes/ticket_notifications.py
index 61e43e37..c636cf64 100644
--- a/app/cabinet/routes/ticket_notifications.py
+++ b/app/cabinet/routes/ticket_notifications.py
@@ -1,8 +1,8 @@
"""Ticket notifications routes for cabinet."""
-import logging
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +13,7 @@ from app.database.models import User
from ..dependencies import get_cabinet_db, get_current_admin_user, get_current_cabinet_user
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/tickets/notifications', tags=['Cabinet Ticket Notifications'])
admin_router = APIRouter(prefix='/admin/tickets/notifications', tags=['Cabinet Admin Ticket Notifications'])
diff --git a/app/cabinet/routes/tickets.py b/app/cabinet/routes/tickets.py
index b28e6f9b..9f96650e 100644
--- a/app/cabinet/routes/tickets.py
+++ b/app/cabinet/routes/tickets.py
@@ -1,9 +1,9 @@
"""Support tickets routes for cabinet."""
-import logging
import math
from datetime import datetime
+import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -26,7 +26,7 @@ from ..schemas.tickets import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/tickets', tags=['Cabinet Tickets'])
@@ -164,7 +164,7 @@ async def create_ticket(
try:
await notify_admins_about_new_ticket(ticket, db)
except Exception as e:
- logger.error(f'Error notifying admins about new ticket from cabinet: {e}')
+ logger.error('Error notifying admins about new ticket from cabinet', error=e)
# Уведомить админов в кабинете
try:
@@ -173,7 +173,7 @@ async def create_ticket(
# Отправить WebSocket уведомление
await notify_admins_new_ticket(ticket.id, ticket.title, user.id)
except Exception as e:
- logger.error(f'Error creating cabinet notification for new ticket: {e}')
+ logger.error('Error creating cabinet notification for new ticket', error=e)
messages = [_message_to_response(m) for m in ticket.messages]
@@ -284,7 +284,7 @@ async def add_ticket_message(
try:
await notify_admins_about_ticket_reply(ticket, request.message, db)
except Exception as e:
- logger.error(f'Error notifying admins about ticket reply from cabinet: {e}')
+ logger.error('Error notifying admins about ticket reply from cabinet', error=e)
# Уведомить админов в кабинете
try:
@@ -295,6 +295,6 @@ async def add_ticket_message(
# Отправить WebSocket уведомление
await notify_admins_ticket_reply(ticket.id, (request.message or '')[:100], user.id)
except Exception as e:
- logger.error(f'Error creating cabinet notification for user reply: {e}')
+ logger.error('Error creating cabinet notification for user reply', error=e)
return _message_to_response(message)
diff --git a/app/cabinet/routes/websocket.py b/app/cabinet/routes/websocket.py
index 60665552..5066b2b3 100644
--- a/app/cabinet/routes/websocket.py
+++ b/app/cabinet/routes/websocket.py
@@ -4,8 +4,8 @@ from __future__ import annotations
import asyncio
import json
-import logging
+import structlog
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.cabinet.auth.jwt_handler import get_token_payload
@@ -14,7 +14,7 @@ from app.database.crud.user import get_user_by_id
from app.database.database import AsyncSessionLocal
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter()
@@ -42,10 +42,10 @@ class CabinetConnectionManager:
self._admin_connections[user_id].add(websocket)
logger.debug(
- 'Cabinet WS connected: user_id=%d, is_admin=%s, total_users=%d',
- user_id,
- is_admin,
- len(self._user_connections),
+ 'Cabinet WS connected: user_id is_admin total_users',
+ user_id=user_id,
+ is_admin=is_admin,
+ user_connections_count=len(self._user_connections),
)
async def disconnect(self, websocket: WebSocket, user_id: int) -> None:
@@ -61,7 +61,7 @@ class CabinetConnectionManager:
if not self._admin_connections[user_id]:
del self._admin_connections[user_id]
- logger.debug('Cabinet WS disconnected: user_id=%d', user_id)
+ logger.debug('Cabinet WS disconnected: user_id', user_id=user_id)
async def send_to_user(self, user_id: int, message: dict) -> None:
"""Отправить сообщение конкретному пользователю."""
@@ -79,7 +79,7 @@ class CabinetConnectionManager:
try:
await ws.send_text(data)
except Exception as e:
- logger.warning('Failed to send to user %d: %s', user_id, e)
+ logger.warning('Failed to send to user', user_id=user_id, e=e)
disconnected.add(ws)
# Cleanup disconnected
@@ -105,7 +105,7 @@ class CabinetConnectionManager:
try:
await ws.send_text(data)
except Exception as e:
- logger.warning('Failed to send to admin %d: %s', user_id, e)
+ logger.warning('Failed to send to admin', user_id=user_id, e=e)
if user_id not in disconnected_by_user:
disconnected_by_user[user_id] = set()
disconnected_by_user[user_id].add(ws)
@@ -152,7 +152,7 @@ async def verify_cabinet_ws_token(token: str) -> tuple[int | None, bool]:
)
return user_id, is_admin
except (TimeoutError, OSError, ConnectionRefusedError) as e:
- logger.error('Database connection error in WS token verification: %s', str(e)[:200])
+ logger.error('Database connection error in WS token verification', e=str(e)[:200])
return None, False
@@ -165,7 +165,7 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
token = websocket.query_params.get('token')
if not token:
- logger.debug('Cabinet WS: No token from %s', client_host)
+ logger.debug('Cabinet WS: No token from', client_host=client_host)
# Принимаем и сразу закрываем с кодом ошибки
await websocket.accept()
await websocket.close(code=1008, reason='Unauthorized: No token')
@@ -175,7 +175,7 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
user_id, is_admin = await verify_cabinet_ws_token(token)
if not user_id:
- logger.debug('Cabinet WS: Invalid token from %s', client_host)
+ logger.debug('Cabinet WS: Invalid token from', client_host=client_host)
# Принимаем и сразу закрываем с кодом ошибки
await websocket.accept()
await websocket.close(code=1008, reason='Unauthorized: Invalid token')
@@ -184,9 +184,9 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
# Принимаем соединение
try:
await websocket.accept()
- logger.debug('Cabinet WS accepted: user_id=%d, is_admin=%s', user_id, is_admin)
+ logger.debug('Cabinet WS accepted: user_id is_admin', user_id=user_id, is_admin=is_admin)
except Exception as e:
- logger.error('Cabinet WS: Failed to accept from %s: %s', client_host, e)
+ logger.error('Cabinet WS: Failed to accept from', client_host=client_host, e=e)
return
# Регистрируем подключение
@@ -213,17 +213,17 @@ async def cabinet_websocket_endpoint(websocket: WebSocket):
await websocket.send_json({'type': 'pong'})
except json.JSONDecodeError:
- logger.warning('Cabinet WS: Invalid JSON from user %d', user_id)
+ logger.warning('Cabinet WS: Invalid JSON from user', user_id=user_id)
except WebSocketDisconnect:
break
except Exception as e:
- logger.exception('Cabinet WS error for user %d: %s', user_id, e)
+ logger.exception('Cabinet WS error for user', user_id=user_id, e=e)
break
except WebSocketDisconnect:
- logger.debug('Cabinet WS disconnected: user_id=%d', user_id)
+ logger.debug('Cabinet WS disconnected: user_id', user_id=user_id)
except Exception as e:
- logger.exception('Cabinet WS error: %s', e)
+ logger.exception('Cabinet WS error', e=e)
finally:
await cabinet_ws_manager.disconnect(websocket, user_id)
diff --git a/app/cabinet/routes/wheel.py b/app/cabinet/routes/wheel.py
index 4aa31450..b509478f 100644
--- a/app/cabinet/routes/wheel.py
+++ b/app/cabinet/routes/wheel.py
@@ -2,11 +2,11 @@
API роуты колеса удачи для пользователей.
"""
-import logging
import math
import time
import httpx
+import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
@@ -32,7 +32,7 @@ from app.database.models import User
from app.services.wheel_service import wheel_service
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/wheel', tags=['Fortune Wheel'])
@@ -253,14 +253,16 @@ async def create_stars_invoice(
result = response.json()
if not result.get('ok'):
- logger.error(f'Telegram API error: {result}')
+ logger.error('Telegram API error', result=result)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Ошибка создания инвойса',
)
invoice_url = result['result']
- logger.info(f'Created Stars invoice for wheel spin: user={user.id}, stars={stars_amount}')
+ logger.info(
+ 'Created Stars invoice for wheel spin: user=, stars', user_id=user.id, stars_amount=stars_amount
+ )
return StarsInvoiceResponse(
invoice_url=invoice_url,
@@ -268,7 +270,7 @@ async def create_stars_invoice(
)
except httpx.HTTPError as e:
- logger.error(f'HTTP error creating invoice: {e}')
+ logger.error('HTTP error creating invoice', error=e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Ошибка соединения с Telegram',
diff --git a/app/cabinet/services/email_service.py b/app/cabinet/services/email_service.py
index 9818892d..bc833a8f 100644
--- a/app/cabinet/services/email_service.py
+++ b/app/cabinet/services/email_service.py
@@ -1,14 +1,15 @@
"""Email service for sending verification and password reset emails."""
-import logging
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
+import structlog
+
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class EmailService:
@@ -41,7 +42,7 @@ class EmailService:
if smtp.has_extn('auth'):
smtp.login(self.user, self.password)
else:
- logger.debug(f'SMTP server {self.host} does not support AUTH, skipping authentication')
+ logger.debug('SMTP server does not support AUTH, skipping authentication', host=self.host)
return smtp
@@ -94,11 +95,11 @@ class EmailService:
with self._get_smtp_connection() as smtp:
smtp.sendmail(self.from_email, to_email, msg.as_string())
- logger.info(f'Email sent successfully to {to_email}')
+ logger.info('Email sent successfully to', to_email=to_email)
return True
except Exception as e:
- logger.error(f'Failed to send email to {to_email}: {e}')
+ logger.error('Failed to send email to', to_email=to_email, error=e)
return False
def send_verification_email(
diff --git a/app/cabinet/services/email_template_overrides.py b/app/cabinet/services/email_template_overrides.py
index 1e3ca6e9..f8bfae09 100644
--- a/app/cabinet/services/email_template_overrides.py
+++ b/app/cabinet/services/email_template_overrides.py
@@ -4,17 +4,17 @@ Service for managing email template overrides stored in the database.
Custom templates override the hardcoded defaults from email_templates.py.
"""
-import logging
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.database import AsyncSessionLocal
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_template_override(
@@ -56,7 +56,9 @@ async def get_template_override(
return None
except Exception as e:
- logger.debug('Не удалось получить override шаблона %s/%s: %s', notification_type, language, e)
+ logger.debug(
+ 'Не удалось получить override шаблона /', notification_type=notification_type, language=language, e=e
+ )
return None
diff --git a/app/config.py b/app/config.py
index 04efa9b4..31dc929c 100644
--- a/app/config.py
+++ b/app/config.py
@@ -1,7 +1,6 @@
import hashlib
import hmac
import html
-import logging
import math
import os
import re
@@ -11,6 +10,7 @@ from pathlib import Path
from urllib.parse import urlparse
from zoneinfo import ZoneInfo
+import structlog
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
@@ -23,7 +23,7 @@ DEFAULT_DISPLAY_NAME_BANNED_KEYWORDS: list[str] = [
USER_TAG_PATTERN = re.compile(r'^[A-Z0-9_]{1,16}$')
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class Settings(BaseSettings):
@@ -1297,7 +1297,7 @@ class Settings(BaseSettings):
raise ValueError
return time(hour=hours, minute=minutes)
except (ValueError, AttributeError):
- logging.getLogger(__name__).warning('Некорректное значение ADMIN_REPORTS_SEND_TIME: %s', value)
+ logger.warning('Некорректное значение ADMIN_REPORTS_SEND_TIME', send_time_value=value)
return None
def kopeks_to_rubles(self, kopeks: int) -> float:
@@ -1317,17 +1317,14 @@ class Settings(BaseSettings):
if len(cleaned) > 16:
logger.warning(
- 'Некорректная длина %s: максимум 16 символов, получено %s',
- setting_name,
- len(cleaned),
+ 'Некорректная длина : максимум 16 символов, получено',
+ setting_name=setting_name,
+ cleaned_count=len(cleaned),
)
return None
if not USER_TAG_PATTERN.fullmatch(cleaned):
- logger.warning(
- 'Некорректный формат %s: допустимы только A-Z, 0-9 и подчёркивание',
- setting_name,
- )
+ logger.warning('Некорректный формат : допустимы только A-Z, 0-9 и подчёркивание', setting_name=setting_name)
return None
return cleaned
@@ -1448,9 +1445,9 @@ class Settings(BaseSettings):
try:
return int(self.EXTERNAL_ADMIN_TOKEN_BOT_ID) if self.EXTERNAL_ADMIN_TOKEN_BOT_ID else None
except (TypeError, ValueError): # pragma: no cover - защитная ветка для некорректных значений
- logging.getLogger(__name__).warning(
- 'Некорректный идентификатор бота для внешней админки: %s',
- self.EXTERNAL_ADMIN_TOKEN_BOT_ID,
+ logger.warning(
+ 'Некорректный идентификатор бота для внешней админки',
+ EXTERNAL_ADMIN_TOKEN_BOT_ID=self.EXTERNAL_ADMIN_TOKEN_BOT_ID,
)
return None
@@ -1531,10 +1528,7 @@ class Settings(BaseSettings):
try:
value = int(raw_value)
except (TypeError, ValueError):
- logger.warning(
- 'Некорректное значение DEVICES_SELECTION_DISABLED_AMOUNT: %s',
- raw_value,
- )
+ logger.warning('Некорректное значение DEVICES_SELECTION_DISABLED_AMOUNT', raw_value=raw_value)
return None
if value < 0:
@@ -1574,8 +1568,7 @@ class Settings(BaseSettings):
value = int(self.TRIAL_ACTIVATION_PRICE)
except (TypeError, ValueError):
logger.warning(
- 'Некорректное значение TRIAL_ACTIVATION_PRICE: %s',
- self.TRIAL_ACTIVATION_PRICE,
+ 'Некорректное значение TRIAL_ACTIVATION_PRICE', TRIAL_ACTIVATION_PRICE=self.TRIAL_ACTIVATION_PRICE
)
return 0
@@ -1694,7 +1687,7 @@ class Settings(BaseSettings):
try:
method_code = int(part)
except ValueError:
- logger.warning('Некорректный код метода Platega: %s', part)
+ logger.warning('Некорректный код метода Platega', part=part)
continue
if method_code in {2, 10, 11, 12, 13} and method_code not in seen:
methods.append(method_code)
@@ -1789,8 +1782,8 @@ class Settings(BaseSettings):
if minutes <= 0:
logger.warning(
- 'Некорректный интервал автопроверки платежей: %s. Используется значение по умолчанию 10 минут.',
- self.PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES,
+ 'Некорректный интервал автопроверки платежей: . Используется значение по умолчанию 10 минут.',
+ PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES=self.PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES,
)
return 10
@@ -2144,22 +2137,13 @@ class Settings(BaseSettings):
return self.REFERRAL_NOTIFICATIONS_ENABLED
def get_traffic_packages(self) -> list[dict]:
- import logging
-
- logger = logging.getLogger(__name__)
-
try:
packages = []
config_str = self.TRAFFIC_PACKAGES_CONFIG.strip()
- logger.debug(f"CONFIG STRING: '{config_str}'")
-
if not config_str:
- logger.debug('CONFIG EMPTY, USING FALLBACK')
return self._get_fallback_traffic_packages()
- logger.debug('PARSING CONFIG...')
-
for package_config in config_str.split(','):
package_config = package_config.strip()
if not package_config:
@@ -2178,11 +2162,10 @@ class Settings(BaseSettings):
except ValueError:
continue
- logger.debug(f'PARSED {len(packages)} packages from config')
return packages if packages else self._get_fallback_traffic_packages()
except Exception as e:
- logger.info(f'ERROR PARSING CONFIG: {e}')
+ logger.warning('ERROR PARSING CONFIG', error=e)
return self._get_fallback_traffic_packages()
def is_version_check_enabled(self) -> bool:
diff --git a/app/database/crud/campaign.py b/app/database/crud/campaign.py
index 9ada95ae..d36df4b4 100644
--- a/app/database/crud/campaign.py
+++ b/app/database/crud/campaign.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import and_, delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -17,7 +17,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_campaign(
@@ -57,10 +57,10 @@ async def create_campaign(
await db.refresh(campaign)
logger.info(
- '📣 Создана рекламная кампания %s (start=%s, bonus=%s)',
- campaign.name,
- campaign.start_parameter,
- campaign.bonus_type,
+ '📣 Создана рекламная кампания (start bonus=)',
+ campaign_name=campaign.name,
+ start_parameter=campaign.start_parameter,
+ bonus_type=campaign.bonus_type,
)
return campaign
@@ -157,14 +157,14 @@ async def update_campaign(
await db.commit()
await db.refresh(campaign)
- logger.info('✏️ Обновлена рекламная кампания %s (%s)', campaign.name, update_data)
+ logger.info('✏️ Обновлена рекламная кампания', campaign_name=campaign.name, update_data=update_data)
return campaign
async def delete_campaign(db: AsyncSession, campaign: AdvertisingCampaign) -> bool:
await db.execute(delete(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id))
await db.commit()
- logger.info('🗑️ Удалена рекламная кампания %s', campaign.name)
+ logger.info('🗑️ Удалена рекламная кампания', campaign_name=campaign.name)
return True
@@ -217,7 +217,7 @@ async def record_campaign_registration(
await db.commit()
await db.refresh(registration)
- logger.info('📈 Регистрируем пользователя %s в кампании %s', user_id, campaign_id)
+ logger.info('📈 Регистрируем пользователя в кампании', user_id=user_id, campaign_id=campaign_id)
return registration
diff --git a/app/database/crud/cloudpayments.py b/app/database/crud/cloudpayments.py
index 7f40d29f..0ca32e90 100644
--- a/app/database/crud/cloudpayments.py
+++ b/app/database/crud/cloudpayments.py
@@ -2,17 +2,17 @@
from __future__ import annotations
-import logging
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import CloudPaymentsPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_cloudpayments_payment(
@@ -65,10 +65,10 @@ async def create_cloudpayments_payment(
await db.refresh(payment)
logger.debug(
- 'Created CloudPayments payment: id=%s, invoice=%s, amount=%s',
- payment.id,
- invoice_id,
- amount_kopeks,
+ 'Created CloudPayments payment: id invoice amount',
+ payment_id=payment.id,
+ invoice_id=invoice_id,
+ amount_kopeks=amount_kopeks,
)
return payment
@@ -195,9 +195,7 @@ async def mark_cloudpayments_payment_as_paid(
await db.refresh(payment)
logger.info(
- 'Marked CloudPayments payment as paid: id=%s, invoice=%s',
- payment.id,
- payment.invoice_id,
+ 'Marked CloudPayments payment as paid: id invoice', payment_id=payment.id, invoice_id=payment.invoice_id
)
return payment
diff --git a/app/database/crud/contest.py b/app/database/crud/contest.py
index 511bf6ee..5c100b2d 100644
--- a/app/database/crud/contest.py
+++ b/app/database/crud/contest.py
@@ -1,7 +1,7 @@
-import logging
from collections.abc import Sequence
from datetime import datetime
+import structlog
from sqlalchemy import and_, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import ContestAttempt, ContestRound, ContestTemplate, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Templates
diff --git a/app/database/crud/cryptobot.py b/app/database/crud/cryptobot.py
index 869d77ed..063891b6 100644
--- a/app/database/crud/cryptobot.py
+++ b/app/database/crud/cryptobot.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import CryptoBotPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_cryptobot_payment(
@@ -41,7 +41,13 @@ async def create_cryptobot_payment(
await db.commit()
await db.refresh(payment)
- logger.info(f'Создан CryptoBot платеж: {invoice_id} на {amount} {asset} для пользователя {user_id}')
+ logger.info(
+ 'Создан CryptoBot платеж: на для пользователя',
+ invoice_id=invoice_id,
+ amount=amount,
+ asset=asset,
+ user_id=user_id,
+ )
return payment
@@ -78,7 +84,7 @@ async def update_cryptobot_payment_status(
await db.commit()
await db.refresh(payment)
- logger.info(f'Обновлен статус CryptoBot платежа {invoice_id}: {status}')
+ logger.info('Обновлен статус CryptoBot платежа', invoice_id=invoice_id, status=status)
return payment
@@ -96,7 +102,7 @@ async def link_cryptobot_payment_to_transaction(
await db.commit()
await db.refresh(payment)
- logger.info(f'Связан CryptoBot платеж {invoice_id} с транзакцией {transaction_id}')
+ logger.info('Связан CryptoBot платеж с транзакцией', invoice_id=invoice_id, transaction_id=transaction_id)
return payment
diff --git a/app/database/crud/discount_offer.py b/app/database/crud/discount_offer.py
index 6d841666..91de782e 100644
--- a/app/database/crud/discount_offer.py
+++ b/app/database/crud/discount_offer.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-import logging
from datetime import datetime, timedelta
+import structlog
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -11,7 +11,7 @@ from app.database.crud.promo_offer_log import log_promo_offer_action
from app.database.models import DiscountOffer
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def upsert_discount_offer(
@@ -178,17 +178,12 @@ async def mark_offer_claimed(
details=details,
)
except Exception as exc: # pragma: no cover - defensive logging
- logger.warning(
- 'Failed to record promo offer claim log for offer %s: %s',
- offer.id,
- exc,
- )
+ logger.warning('Failed to record promo offer claim log for offer', offer_id=offer.id, exc=exc)
try:
await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to rollback session after promo offer claim log failure: %s',
- rollback_error,
+ 'Failed to rollback session after promo offer claim log failure', rollback_error=rollback_error
)
return offer
@@ -239,16 +234,13 @@ async def deactivate_expired_offers(db: AsyncSession) -> int:
)
except Exception as exc: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to record promo offer disable log for offer %s: %s',
- payload.get('offer_id'),
- exc,
+ 'Failed to record promo offer disable log for offer', payload=payload.get('offer_id'), exc=exc
)
try:
await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to rollback session after promo offer disable log failure: %s',
- rollback_error,
+ 'Failed to rollback session after promo offer disable log failure', rollback_error=rollback_error
)
return count
diff --git a/app/database/crud/faq.py b/app/database/crud/faq.py
index a42d2520..84ccb371 100644
--- a/app/database/crud/faq.py
+++ b/app/database/crud/faq.py
@@ -1,14 +1,14 @@
-import logging
from collections.abc import Iterable
from datetime import datetime
+import structlog
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import FaqPage, FaqSetting
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_faq_setting(db: AsyncSession, language: str) -> FaqSetting | None:
@@ -94,7 +94,7 @@ async def create_faq_page(
await db.commit()
await db.refresh(page)
- logger.info('✅ Создана страница FAQ %s для языка %s', page.id, language)
+ logger.info('✅ Создана страница FAQ для языка', page_id=page.id, language=language)
return page
@@ -122,7 +122,7 @@ async def update_faq_page(
await db.commit()
await db.refresh(page)
- logger.info('✅ Страница FAQ %s обновлена', page.id)
+ logger.info('✅ Страница FAQ обновлена', page_id=page.id)
return page
@@ -130,7 +130,7 @@ async def update_faq_page(
async def delete_faq_page(db: AsyncSession, page_id: int) -> None:
await db.execute(delete(FaqPage).where(FaqPage.id == page_id))
await db.commit()
- logger.info('🗑️ Страница FAQ %s удалена', page_id)
+ logger.info('🗑️ Страница FAQ удалена', page_id=page_id)
async def bulk_update_order(
diff --git a/app/database/crud/freekassa.py b/app/database/crud/freekassa.py
index aad9889c..31ba2b73 100644
--- a/app/database/crud/freekassa.py
+++ b/app/database/crud/freekassa.py
@@ -1,16 +1,16 @@
"""CRUD операции для платежей Freekassa."""
import json
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import FreekassaPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_freekassa_payment(
@@ -41,7 +41,7 @@ async def create_freekassa_payment(
db.add(payment)
await db.commit()
await db.refresh(payment)
- logger.info(f'Создан платеж Freekassa: order_id={order_id}, user_id={user_id}')
+ logger.info('Создан платеж Freekassa: order_id=, user_id', order_id=order_id, user_id=user_id)
return payment
@@ -92,7 +92,12 @@ async def update_freekassa_payment_status(
await db.commit()
await db.refresh(payment)
- logger.info(f'Обновлен статус платежа Freekassa: order_id={payment.order_id}, status={status}, is_paid={is_paid}')
+ logger.info(
+ 'Обновлен статус платежа Freekassa: order_id=, status=, is_paid',
+ order_id=payment.order_id,
+ status=status,
+ is_paid=is_paid,
+ )
return payment
diff --git a/app/database/crud/heleket.py b/app/database/crud/heleket.py
index b0ceae99..19e7f34a 100644
--- a/app/database/crud/heleket.py
+++ b/app/database/crud/heleket.py
@@ -1,7 +1,7 @@
-import logging
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import HeleketPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_heleket_payment(
@@ -50,12 +50,12 @@ async def create_heleket_payment(
await db.refresh(payment)
logger.info(
- 'Создан Heleket платеж: uuid=%s order_id=%s amount=%s %s для пользователя %s',
- uuid,
- order_id,
- amount,
- currency,
- user_id,
+ 'Создан Heleket платеж: uuid= order_id= amount= для пользователя',
+ uuid=uuid,
+ order_id=order_id,
+ amount=amount,
+ currency=currency,
+ user_id=user_id,
)
return payment
@@ -107,7 +107,7 @@ async def update_heleket_payment(
payment = await get_heleket_payment_by_uuid(db, uuid)
if not payment:
- logger.error('Heleket платеж с uuid=%s не найден', uuid)
+ logger.error('Heleket платеж с uuid= не найден', uuid=uuid)
return None
if status is not None:
@@ -135,11 +135,11 @@ async def update_heleket_payment(
await db.refresh(payment)
logger.info(
- 'Обновлен Heleket платеж %s: статус=%s payer_amount=%s %s',
- uuid,
- payment.status,
- payment.payer_amount,
- payment.payer_currency,
+ 'Обновлен Heleket платеж : статус= payer_amount',
+ uuid=uuid,
+ payment_status=payment.status,
+ payer_amount=payment.payer_amount,
+ payer_currency=payment.payer_currency,
)
return payment
@@ -153,7 +153,7 @@ async def link_heleket_payment_to_transaction(
payment = await get_heleket_payment_by_uuid(db, uuid)
if not payment:
- logger.error('Не найден Heleket платеж для связи с транзакцией: %s', uuid)
+ logger.error('Не найден Heleket платеж для связи с транзакцией', uuid=uuid)
return None
payment.transaction_id = transaction_id
@@ -162,10 +162,6 @@ async def link_heleket_payment_to_transaction(
await db.commit()
await db.refresh(payment)
- logger.info(
- 'Heleket платеж %s связан с транзакцией %s',
- uuid,
- transaction_id,
- )
+ logger.info('Heleket платеж связан с транзакцией', uuid=uuid, transaction_id=transaction_id)
return payment
diff --git a/app/database/crud/kassa_ai.py b/app/database/crud/kassa_ai.py
index 34aefb71..9b8cae01 100644
--- a/app/database/crud/kassa_ai.py
+++ b/app/database/crud/kassa_ai.py
@@ -1,16 +1,16 @@
"""CRUD операции для платежей KassaAI."""
import json
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import KassaAiPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_kassa_ai_payment(
@@ -43,7 +43,7 @@ async def create_kassa_ai_payment(
db.add(payment)
await db.commit()
await db.refresh(payment)
- logger.info(f'Создан платеж KassaAI: order_id={order_id}, user_id={user_id}')
+ logger.info('Создан платеж KassaAI: order_id=, user_id', order_id=order_id, user_id=user_id)
return payment
@@ -94,7 +94,12 @@ async def update_kassa_ai_payment_status(
await db.commit()
await db.refresh(payment)
- logger.info(f'Обновлен статус платежа KassaAI: order_id={payment.order_id}, status={status}, is_paid={is_paid}')
+ logger.info(
+ 'Обновлен статус платежа KassaAI: order_id=, status=, is_paid',
+ order_id=payment.order_id,
+ status=status,
+ is_paid=is_paid,
+ )
return payment
diff --git a/app/database/crud/mulenpay.py b/app/database/crud/mulenpay.py
index b6cb1ecb..44fe1e46 100644
--- a/app/database/crud/mulenpay.py
+++ b/app/database/crud/mulenpay.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,7 +8,7 @@ from app.config import settings
from app.database.models import MulenPayPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_mulenpay_payment(
@@ -41,12 +41,12 @@ async def create_mulenpay_payment(
await db.refresh(payment)
logger.info(
- 'Создан %s платеж #%s (uuid=%s) на сумму %s копеек для пользователя %s',
- settings.get_mulenpay_display_name(),
- payment.mulen_payment_id,
- uuid,
- amount_kopeks,
- user_id,
+ 'Создан платеж # (uuid=) на сумму копеек для пользователя',
+ get_mulenpay_display_name=settings.get_mulenpay_display_name(),
+ mulen_payment_id=payment.mulen_payment_id,
+ uuid=uuid,
+ amount_kopeks=amount_kopeks,
+ user_id=user_id,
)
return payment
diff --git a/app/database/crud/notification.py b/app/database/crud/notification.py
index eb26ebdc..bc369b21 100644
--- a/app/database/crud/notification.py
+++ b/app/database/crud/notification.py
@@ -1,12 +1,11 @@
-import logging
-
+import structlog
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import SentNotification
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def notification_sent(
diff --git a/app/database/crud/pal24.py b/app/database/crud/pal24.py
index 0eb0087a..50c042bd 100644
--- a/app/database/crud/pal24.py
+++ b/app/database/crud/pal24.py
@@ -2,17 +2,17 @@
from __future__ import annotations
-import logging
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import Pal24Payment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_pal24_payment(
@@ -51,11 +51,11 @@ async def create_pal24_payment(
await db.refresh(payment)
logger.info(
- 'Создан Pal24 платеж #%s для пользователя %s: %s копеек (статус %s)',
- payment.id,
- user_id,
- amount_kopeks,
- status,
+ 'Создан Pal24 платеж # для пользователя : копеек (статус)',
+ payment_id=payment.id,
+ user_id=user_id,
+ amount_kopeks=amount_kopeks,
+ status=status,
)
return payment
@@ -128,10 +128,10 @@ async def update_pal24_payment_status(
await db.refresh(payment)
logger.info(
- 'Обновлен Pal24 платеж %s: статус=%s, is_paid=%s',
- payment.bill_id,
- payment.status,
- payment.is_paid,
+ 'Обновлен Pal24 платеж : статус is_paid',
+ bill_id=payment.bill_id,
+ payment_status=payment.status,
+ is_paid=payment.is_paid,
)
return payment
@@ -145,9 +145,5 @@ async def link_pal24_payment_to_transaction(
await db.execute(update(Pal24Payment).where(Pal24Payment.id == payment.id).values(transaction_id=transaction_id))
await db.commit()
await db.refresh(payment)
- logger.info(
- 'Pal24 платеж %s привязан к транзакции %s',
- payment.bill_id,
- transaction_id,
- )
+ logger.info('Pal24 платеж привязан к транзакции', bill_id=payment.bill_id, transaction_id=transaction_id)
return payment
diff --git a/app/database/crud/platega.py b/app/database/crud/platega.py
index 830b2536..70172e83 100644
--- a/app/database/crud/platega.py
+++ b/app/database/crud/platega.py
@@ -2,17 +2,17 @@
from __future__ import annotations
-import logging
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PlategaPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_platega_payment(
@@ -55,11 +55,11 @@ async def create_platega_payment(
await db.refresh(payment)
logger.info(
- 'Создан Platega платеж #%s (tx=%s) на сумму %s копеек для пользователя %s',
- payment.id,
- platega_transaction_id,
- amount_kopeks,
- user_id,
+ 'Создан Platega платеж # (tx=) на сумму копеек для пользователя',
+ payment_id=payment.id,
+ platega_transaction_id=platega_transaction_id,
+ amount_kopeks=amount_kopeks,
+ user_id=user_id,
)
return payment
diff --git a/app/database/crud/poll.py b/app/database/crud/poll.py
index a286f6ce..339890b2 100644
--- a/app/database/crud/poll.py
+++ b/app/database/crud/poll.py
@@ -1,6 +1,6 @@
-import logging
from collections.abc import Iterable, Sequence
+import structlog
from sqlalchemy import and_, delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -14,7 +14,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_poll(
@@ -97,7 +97,7 @@ async def delete_poll(db: AsyncSession, poll_id: int) -> bool:
await db.delete(poll)
await db.commit()
- logger.info('🗑️ Удалён опрос %s', poll_id)
+ logger.info('🗑️ Удалён опрос', poll_id=poll_id)
return True
diff --git a/app/database/crud/privacy_policy.py b/app/database/crud/privacy_policy.py
index fa7f883a..b9e68abd 100644
--- a/app/database/crud/privacy_policy.py
+++ b/app/database/crud/privacy_policy.py
@@ -1,13 +1,13 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PrivacyPolicy
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_privacy_policy(db: AsyncSession, language: str) -> PrivacyPolicy | None:
@@ -38,11 +38,7 @@ async def upsert_privacy_policy(
await db.commit()
await db.refresh(policy)
- logger.info(
- '✅ Политика конфиденциальности для языка %s обновлена (ID: %s)',
- language,
- policy.id,
- )
+ logger.info('✅ Политика конфиденциальности для языка обновлена (ID:)', language=language, policy_id=policy.id)
return policy
diff --git a/app/database/crud/promo_group.py b/app/database/crud/promo_group.py
index 2b4c96ee..14b55604 100644
--- a/app/database/crud/promo_group.py
+++ b/app/database/crud/promo_group.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -25,7 +24,7 @@ def _normalize_period_discounts(period_discounts: dict[int, int] | None) -> dict
return normalized
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_promo_groups_with_counts(
@@ -194,11 +193,7 @@ async def update_promo_group(
await db.commit()
await db.refresh(group)
- logger.info(
- "Обновлена промогруппа '%s' (id=%s)",
- group.name,
- group.id,
- )
+ logger.info("Обновлена промогруппа '' (id=)", group_name=group.name, group_id=group.id)
return group
@@ -246,10 +241,10 @@ async def delete_promo_group(db: AsyncSession, group: PromoGroup) -> bool:
await db.commit()
logger.info(
- "Промогруппа '%s' (id=%s) удалена, пользователи переведены в '%s'",
- group.name,
- group.id,
- default_group.name,
+ "Промогруппа '' (id=) удалена, пользователи переведены в ''",
+ group_name=group.name,
+ group_id=group.id,
+ default_group_name=default_group.name,
)
return True
diff --git a/app/database/crud/promo_offer_log.py b/app/database/crud/promo_offer_log.py
index d96588d5..dba33e45 100644
--- a/app/database/crud/promo_offer_log.py
+++ b/app/database/crud/promo_offer_log.py
@@ -1,7 +1,6 @@
from __future__ import annotations
-import logging
-
+import structlog
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -9,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoOfferLog
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def log_promo_offer_action(
diff --git a/app/database/crud/promocode.py b/app/database/crud/promocode.py
index 6e00c41c..ca51b1fd 100644
--- a/app/database/crud/promocode.py
+++ b/app/database/crud/promocode.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoCode, PromoCodeType, PromoCodeUse, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_promocode_by_code(db: AsyncSession, code: str) -> PromoCode | None:
@@ -78,9 +78,9 @@ async def create_promocode(
await db.refresh(promocode)
if promo_group_id:
- logger.info(f'✅ Создан промокод: {code} с промогруппой ID {promo_group_id}')
+ logger.info('✅ Создан промокод: с промогруппой ID', code=code, promo_group_id=promo_group_id)
else:
- logger.info(f'✅ Создан промокод: {code}')
+ logger.info('✅ Создан промокод', code=code)
return promocode
@@ -97,11 +97,11 @@ async def use_promocode(db: AsyncSession, promocode_id: int, user_id: int) -> bo
await db.commit()
- logger.info(f'✅ Промокод {promocode.code} использован пользователем {user_id}')
+ logger.info('✅ Промокод использован пользователем', code=promocode.code, user_id=user_id)
return True
except Exception as e:
- logger.error(f'Ошибка использования промокода: {e}')
+ logger.error('Ошибка использования промокода', error=e)
await db.rollback()
return False
@@ -120,7 +120,7 @@ async def create_promocode_use(db: AsyncSession, promocode_id: int, user_id: int
await db.commit()
await db.refresh(promocode_use)
- logger.info(f'📝 Записано использование промокода {promocode_id} пользователем {user_id}')
+ logger.info('📝 Записано использование промокода пользователем', promocode_id=promocode_id, user_id=user_id)
return promocode_use
@@ -194,11 +194,11 @@ async def delete_promocode(db: AsyncSession, promocode: PromoCode) -> bool:
await db.delete(promocode)
await db.commit()
- logger.info(f'🗑️ Удален промокод: {promocode.code}')
+ logger.info('🗑️ Удален промокод', code=promocode.code)
return True
except Exception as e:
- logger.error(f'Ошибка удаления промокода: {e}')
+ logger.error('Ошибка удаления промокода', error=e)
await db.rollback()
return False
diff --git a/app/database/crud/public_offer.py b/app/database/crud/public_offer.py
index 83a85ab4..8ae9f201 100644
--- a/app/database/crud/public_offer.py
+++ b/app/database/crud/public_offer.py
@@ -1,13 +1,13 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PublicOffer
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_public_offer(db: AsyncSession, language: str) -> PublicOffer | None:
@@ -38,11 +38,7 @@ async def upsert_public_offer(
await db.commit()
await db.refresh(offer)
- logger.info(
- '✅ Публичная оферта для языка %s обновлена (ID: %s)',
- language,
- offer.id,
- )
+ logger.info('✅ Публичная оферта для языка обновлена (ID:)', language=language, offer_id=offer.id)
return offer
diff --git a/app/database/crud/referral.py b/app/database/crud/referral.py
index 12cca9ae..61aa0d33 100644
--- a/app/database/crud/referral.py
+++ b/app/database/crud/referral.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime, timedelta
+import structlog
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import ReferralEarning, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_referral_earning(
@@ -31,7 +31,9 @@ async def create_referral_earning(
await db.commit()
await db.refresh(earning)
- logger.info(f'💰 Создан реферальный заработок: {amount_kopeks / 100}₽ для пользователя {user_id}')
+ logger.info(
+ '💰 Создан реферальный заработок: ₽ для пользователя', amount_kopeks=amount_kopeks / 100, user_id=user_id
+ )
return earning
@@ -211,7 +213,10 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
month_earnings = month_referral_earnings_result.scalar() + month_transaction_earnings_result.scalar()
logger.info(
- f'Реферальная статистика: {users_with_referrals} рефералов, {active_referrers} рефереров, выплачено {total_paid} копеек'
+ 'Реферальная статистика: рефералов, рефереров, выплачено копеек',
+ users_with_referrals=users_with_referrals,
+ active_referrers=active_referrers,
+ total_paid=total_paid,
)
return {
diff --git a/app/database/crud/referral_contest.py b/app/database/crud/referral_contest.py
index df35d676..8b10122b 100644
--- a/app/database/crud/referral_contest.py
+++ b/app/database/crud/referral_contest.py
@@ -1,7 +1,7 @@
-import logging
from collections.abc import Sequence
from datetime import date, datetime, time
+import structlog
from sqlalchemy import and_, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -16,7 +16,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_referral_contest(
@@ -725,7 +725,12 @@ async def sync_contest_events(
# Конец дня: 23:59:59.999999
contest_end = contest_end.replace(hour=23, minute=59, second=59, microsecond=999999)
- logger.info('Синхронизация конкурса %s: период с %s по %s', contest_id, contest_start, contest_end)
+ logger.info(
+ 'Синхронизация конкурса : период с по',
+ contest_id=contest_id,
+ contest_start=contest_start,
+ contest_end=contest_end,
+ )
stats = {
'updated': 0,
@@ -815,7 +820,11 @@ async def sync_contest_events(
# Логируем значительные изменения
if abs(old_amount - total_paid) > 10000: # больше 100 руб разницы
logger.debug(
- 'Событие %s (реферал %s): %s -> %s коп.', event.id, event.referral_id, old_amount, total_paid
+ 'Событие (реферал): -> коп.',
+ event_id=event.id,
+ referral_id=event.referral_id,
+ old_amount=old_amount,
+ total_paid=total_paid,
)
else:
stats['skipped'] += 1
@@ -824,11 +833,11 @@ async def sync_contest_events(
await db.commit()
logger.info(
- 'Синхронизация конкурса %s завершена: обновлено %s, пропущено %s, сумма %s коп.',
- contest_id,
- stats['updated'],
- stats['skipped'],
- stats['total_amount'],
+ 'Синхронизация конкурса завершена: обновлено , пропущено , сумма коп.',
+ contest_id=contest_id,
+ stats=stats['updated'],
+ stats_2=stats['skipped'],
+ stats_3=stats['total_amount'],
)
return stats
@@ -861,7 +870,9 @@ async def cleanup_invalid_contest_events(
if contest_end.hour == 0 and contest_end.minute == 0 and contest_end.second == 0:
contest_end = contest_end.replace(hour=23, minute=59, second=59, microsecond=999999)
- logger.info('Очистка конкурса %s: период с %s по %s', contest_id, contest_start, contest_end)
+ logger.info(
+ 'Очистка конкурса : период с по', contest_id=contest_id, contest_start=contest_start, contest_end=contest_end
+ )
# Считаем сколько было событий до очистки
total_before_result = await db.execute(
@@ -905,11 +916,11 @@ async def cleanup_invalid_contest_events(
remaining = int(remaining_result.scalar_one() or 0)
logger.info(
- 'Очистка конкурса %s завершена: удалено %s невалидных событий, осталось %s валидных (было %s)',
- contest_id,
- deleted,
- remaining,
- total_before,
+ 'Очистка конкурса завершена: удалено невалидных событий, осталось валидных (было)',
+ contest_id=contest_id,
+ deleted=deleted,
+ remaining=remaining,
+ total_before=total_before,
)
return {
diff --git a/app/database/crud/rules.py b/app/database/crud/rules.py
index a98a1b13..729f70a8 100644
--- a/app/database/crud/rules.py
+++ b/app/database/crud/rules.py
@@ -1,13 +1,13 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import ServiceRule
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_rules_by_language(db: AsyncSession, language: str = 'ru') -> ServiceRule | None:
@@ -38,7 +38,7 @@ async def create_or_update_rules(
await db.commit()
await db.refresh(new_rules)
- logger.info(f'✅ Правила для языка {language} обновлены (ID: {new_rules.id})')
+ logger.info('✅ Правила для языка обновлены (ID: )', language=language, new_rules_id=new_rules.id)
return new_rules
@@ -53,12 +53,14 @@ async def clear_all_rules(db: AsyncSession, language: str = 'ru') -> bool:
await db.commit()
rows_affected = result.rowcount
- logger.info(f'✅ Очищены правила для языка {language}. Деактивировано записей: {rows_affected}')
+ logger.info(
+ '✅ Очищены правила для языка . Деактивировано записей', language=language, rows_affected=rows_affected
+ )
return rows_affected > 0
except Exception as e:
- logger.error(f'❌ Ошибка при очистке правил для языка {language}: {e}')
+ logger.error('❌ Ошибка при очистке правил для языка', language=language, error=e)
await db.rollback()
raise
@@ -102,7 +104,7 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
rule_to_restore = result.scalar_one_or_none()
if not rule_to_restore:
- logger.warning(f'Правило с ID {rule_id} не найдено для языка {language}')
+ logger.warning('Правило с ID не найдено для языка', rule_id=rule_id, language=language)
return None
await db.execute(
@@ -119,11 +121,13 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
await db.commit()
await db.refresh(restored_rule)
- logger.info(f'✅ Восстановлена версия правил ID {rule_id} как новое правило ID {restored_rule.id}')
+ logger.info(
+ '✅ Восстановлена версия правил ID как новое правило ID', rule_id=rule_id, restored_rule_id=restored_rule.id
+ )
return restored_rule
except Exception as e:
- logger.error(f'❌ Ошибка при восстановлении правил ID {rule_id}: {e}')
+ logger.error('❌ Ошибка при восстановлении правил ID', rule_id=rule_id, error=e)
await db.rollback()
raise
@@ -156,5 +160,5 @@ async def get_rules_statistics(db: AsyncSession) -> dict:
}
except Exception as e:
- logger.error(f'❌ Ошибка при получении статистики правил: {e}')
+ logger.error('❌ Ошибка при получении статистики правил', error=e)
return {'total_active': 0, 'total_all_time': 0, 'languages': {}, 'total_languages': 0, 'error': str(e)}
diff --git a/app/database/crud/server_squad.py b/app/database/crud/server_squad.py
index 181e02d9..ec3daff5 100644
--- a/app/database/crud/server_squad.py
+++ b/app/database/crud/server_squad.py
@@ -1,8 +1,8 @@
-import logging
import random
from collections.abc import Iterable, Sequence
from datetime import datetime
+import structlog
from sqlalchemy import (
String,
and_,
@@ -28,7 +28,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _get_default_promo_group_id(db: AsyncSession) -> int | None:
@@ -64,7 +64,7 @@ async def create_server_squad(
promo_groups = promo_groups_result.scalars().all()
if len(promo_groups) != len(normalized_group_ids):
- logger.warning('Не все промогруппы найдены при создании сервера %s', display_name)
+ logger.warning('Не все промогруппы найдены при создании сервера', display_name=display_name)
server_squad = ServerSquad(
squad_uuid=squad_uuid,
@@ -84,7 +84,7 @@ async def create_server_squad(
await db.commit()
await db.refresh(server_squad)
- logger.info(f'✅ Создан сервер {display_name} (UUID: {squad_uuid})')
+ logger.info('✅ Создан сервер (UUID: )', display_name=display_name, squad_uuid=squad_uuid)
return server_squad
@@ -267,13 +267,17 @@ async def delete_server_squad(db: AsyncSession, server_id: int) -> bool:
connections_count = connections_result.scalar()
if connections_count > 0:
- logger.warning(f'⚠ Нельзя удалить сервер {server_id}: есть активные подключения ({connections_count})')
+ logger.warning(
+ '⚠ Нельзя удалить сервер есть активные подключения',
+ server_id=server_id,
+ connections_count=connections_count,
+ )
return False
await db.execute(delete(ServerSquad).where(ServerSquad.id == server_id))
await db.commit()
- logger.info(f'🗑️ Удален сервер (ID: {server_id})')
+ logger.info('🗑️ Удален сервер (ID: )', server_id=server_id)
return True
@@ -322,11 +326,7 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
subscription_ids = {row[0] for row in subscription_ids_result.fetchall()}
for server in removed_servers:
- logger.info(
- '🗑️ Удаляется сервер %s (UUID: %s)',
- server.display_name,
- server.squad_uuid,
- )
+ logger.info('🗑️ Удаляется сервер (UUID:)', display_name=server.display_name, squad_uuid=server.squad_uuid)
await db.execute(delete(SubscriptionServer).where(SubscriptionServer.server_squad_id.in_(removed_ids)))
@@ -386,20 +386,14 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
removed = len(removed_servers)
if cleaned_subscriptions:
- logger.info(
- '🧹 Обновлены подписки после удаления серверов: %s',
- cleaned_subscriptions,
- )
+ logger.info('🧹 Обновлены подписки после удаления серверов', cleaned_subscriptions=cleaned_subscriptions)
if cleaned_tariffs:
- logger.info(
- '🧹 Обновлены тарифы после удаления серверов: %s',
- cleaned_tariffs,
- )
+ logger.info('🧹 Обновлены тарифы после удаления серверов', cleaned_tariffs=cleaned_tariffs)
await db.commit()
- logger.info(f'🔄 Синхронизация завершена: +{created} ~{updated} -{removed}')
+ logger.info('🔄 Синхронизация завершена: + ~', created=created, updated=updated, removed=removed)
return created, updated, removed
@@ -767,11 +761,11 @@ async def add_user_to_servers(db: AsyncSession, server_squad_ids: list[int]) ->
)
await db.flush()
- logger.info(f'✅ Увеличен счетчик пользователей для серверов: {server_squad_ids}')
+ logger.info('✅ Увеличен счетчик пользователей для серверов', server_squad_ids=server_squad_ids)
return True
except Exception as e:
- logger.error(f'Ошибка увеличения счетчика пользователей: {e}')
+ logger.error('Ошибка увеличения счетчика пользователей', error=e)
raise
@@ -785,11 +779,11 @@ async def remove_user_from_servers(db: AsyncSession, server_squad_ids: list[int]
)
await db.flush()
- logger.info(f'✅ Уменьшен счетчик пользователей для серверов: {server_squad_ids}')
+ logger.info('✅ Уменьшен счетчик пользователей для серверов', server_squad_ids=server_squad_ids)
return True
except Exception as e:
- logger.error(f'Ошибка уменьшения счетчика пользователей: {e}')
+ logger.error('Ошибка уменьшения счетчика пользователей', error=e)
raise
@@ -836,12 +830,12 @@ async def update_server_user_counts(
await db.flush()
if add_set:
- logger.info('✅ Увеличен счетчик пользователей для серверов: %s', sorted(add_set))
+ logger.info('✅ Увеличен счетчик пользователей для серверов', sorted=sorted(add_set))
if remove_set:
- logger.info('✅ Уменьшен счетчик пользователей для серверов: %s', sorted(remove_set))
+ logger.info('✅ Уменьшен счетчик пользователей для серверов', sorted=sorted(remove_set))
except Exception as e:
- logger.error('Ошибка обновления счетчиков серверов: %s', e)
+ logger.error('Ошибка обновления счетчиков серверов', e=e)
raise
@@ -875,7 +869,7 @@ async def ensure_servers_synced(db: AsyncSession) -> None:
server_count = result.scalar() or 0
if server_count > 0:
- logger.info(f'✅ В базе уже есть {server_count} серверов, пропускаем синхронизацию')
+ logger.info('✅ В базе уже есть серверов, пропускаем синхронизацию', server_count=server_count)
return
logger.info('🔄 Серверов в БД нет, начинаем синхронизацию с RemnaWave...')
@@ -900,10 +894,10 @@ async def ensure_servers_synced(db: AsyncSession) -> None:
# Синхронизируем
created, updated, removed = await sync_with_remnawave(db, squads)
- logger.info(f'✅ Серверы синхронизированы: +{created} ~{updated} -{removed}')
+ logger.info('✅ Серверы синхронизированы: + ~', created=created, updated=updated, removed=removed)
except Exception as e:
- logger.error(f'❌ Ошибка синхронизации серверов: {e}')
+ logger.error('❌ Ошибка синхронизации серверов', error=e)
async def sync_server_user_counts(db: AsyncSession) -> int:
@@ -911,7 +905,7 @@ async def sync_server_user_counts(db: AsyncSession) -> int:
all_servers_result = await db.execute(select(ServerSquad.id, ServerSquad.squad_uuid))
all_servers = all_servers_result.fetchall()
- logger.info(f'🔍 Найдено серверов для синхронизации: {len(all_servers)}')
+ logger.info('🔍 Найдено серверов для синхронизации', all_servers_count=len(all_servers))
updated_count = 0
for server_id, squad_uuid in all_servers:
@@ -926,16 +920,18 @@ async def sync_server_user_counts(db: AsyncSession) -> int:
)
actual_users = count_result.scalar() or 0
- logger.info(f'📊 Сервер {server_id} ({squad_uuid[:8]}): {actual_users} пользователей')
+ logger.info(
+ '📊 Сервер пользователей', server_id=server_id, squad_uuid=squad_uuid[:8], actual_users=actual_users
+ )
await db.execute(update(ServerSquad).where(ServerSquad.id == server_id).values(current_users=actual_users))
updated_count += 1
await db.commit()
- logger.info(f'✅ Синхронизированы счетчики для {updated_count} серверов')
+ logger.info('✅ Синхронизированы счетчики для серверов', updated_count=updated_count)
return updated_count
except Exception as e:
- logger.error(f'Ошибка синхронизации счетчиков пользователей: {e}')
+ logger.error('Ошибка синхронизации счетчиков пользователей', error=e)
await db.rollback()
return 0
diff --git a/app/database/crud/squad.py b/app/database/crud/squad.py
index 4ac97e75..3e518d8e 100644
--- a/app/database/crud/squad.py
+++ b/app/database/crud/squad.py
@@ -1,12 +1,11 @@
-import logging
-
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import Squad
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_squad_by_uuid(db: AsyncSession, uuid: str) -> Squad | None:
@@ -28,7 +27,7 @@ async def create_squad(
await db.commit()
await db.refresh(squad)
- logger.info(f'✅ Создан сквад: {name}')
+ logger.info('✅ Создан сквад', name=name)
return squad
diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py
index d47f2085..20078348 100644
--- a/app/database/crud/subscription.py
+++ b/app/database/crud/subscription.py
@@ -1,8 +1,8 @@
-import logging
from collections.abc import Iterable
from datetime import UTC, datetime, timedelta
from typing import Optional
+import structlog
from sqlalchemy import and_, delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -22,7 +22,7 @@ from app.utils.pricing_utils import calculate_months_from_days, get_remaining_mo
from app.utils.timezone import format_local_datetime
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
_WEBHOOK_GUARD_SECONDS = 60
@@ -50,7 +50,10 @@ async def get_subscription_by_user_id(db: AsyncSession, user_id: int) -> Subscri
if subscription:
logger.info(
- f'🔍 Загружена подписка {subscription.id} для пользователя {user_id}, статус: {subscription.status}'
+ '🔍 Загружена подписка для пользователя статус',
+ subscription_id=subscription.id,
+ user_id=user_id,
+ status=subscription.status,
)
subscription = await check_and_update_subscription_status(db, subscription)
@@ -93,16 +96,10 @@ async def create_trial_subscription(
if random_squad:
final_squads = [random_squad]
logger.debug(
- 'Выбран сквад %s для триальной подписки пользователя %s',
- random_squad,
- user_id,
+ 'Выбран сквад для триальной подписки пользователя', random_squad=random_squad, user_id=user_id
)
except Exception as error:
- logger.error(
- 'Не удалось получить сквад для триальной подписки пользователя %s: %s',
- user_id,
- error,
- )
+ logger.error('Не удалось получить сквад для триальной подписки пользователя', user_id=user_id, error=error)
end_date = datetime.utcnow() + timedelta(days=duration_days)
@@ -119,9 +116,7 @@ async def create_trial_subscription(
await db.commit()
await db.refresh(existing)
logger.info(
- '🎁 Обновлена PENDING триальная подписка %s для пользователя %s',
- existing.id,
- user_id,
+ '🎁 Обновлена PENDING триальная подписка для пользователя', existing_id=existing.id, user_id=user_id
)
return existing
@@ -157,20 +152,14 @@ async def create_trial_subscription(
server_ids = await get_server_ids_by_uuids(db, final_squads)
if server_ids:
await add_user_to_servers(db, server_ids)
- logger.info(
- '📈 Обновлен счетчик пользователей для триальных сквадов %s',
- final_squads,
- )
+ logger.info('📈 Обновлен счетчик пользователей для триальных сквадов', final_squads=final_squads)
else:
- logger.warning(
- '⚠️ Не удалось найти серверы для обновления счетчика (сквады %s)',
- final_squads,
- )
+ logger.warning('⚠️ Не удалось найти серверы для обновления счетчика (сквады)', final_squads=final_squads)
except Exception as error:
logger.error(
- '⚠️ Ошибка обновления счетчика пользователей для триальных сквадов %s: %s',
- final_squads,
- error,
+ '⚠️ Ошибка обновления счетчика пользователей для триальных сквадов',
+ final_squads=final_squads,
+ error=error,
)
return subscription
@@ -211,7 +200,10 @@ async def create_paid_subscription(
await db.refresh(subscription)
logger.info(
- f'💎 Создана платная подписка для пользователя {user_id}, ID: {subscription.id}, статус: {subscription.status}'
+ '💎 Создана платная подписка для пользователя ID: статус',
+ user_id=user_id,
+ subscription_id=subscription.id,
+ status=subscription.status,
)
squad_uuids = list(connected_squads or [])
@@ -226,21 +218,21 @@ async def create_paid_subscription(
if server_ids:
await add_user_to_servers(db, server_ids)
logger.info(
- '📈 Обновлен счетчик пользователей для платной подписки пользователя %s (сквады: %s)',
- user_id,
- squad_uuids,
+ '📈 Обновлен счетчик пользователей для платной подписки пользователя (сквады:)',
+ user_id=user_id,
+ squad_uuids=squad_uuids,
)
else:
logger.warning(
- '⚠️ Не удалось найти серверы для обновления счетчика платной подписки пользователя %s (сквады: %s)',
- user_id,
- squad_uuids,
+ '⚠️ Не удалось найти серверы для обновления счетчика платной подписки пользователя (сквады:)',
+ user_id=user_id,
+ squad_uuids=squad_uuids,
)
except Exception as error:
logger.error(
- '⚠️ Ошибка обновления счетчика пользователей серверов для платной подписки пользователя %s: %s',
- user_id,
- error,
+ '⚠️ Ошибка обновления счетчика пользователей серверов для платной подписки пользователя',
+ user_id=user_id,
+ error=error,
)
return subscription
@@ -313,16 +305,16 @@ async def replace_subscription(
)
logger.info(
- '♻️ Обновлены параметры подписки %s: удалено сквадов %s, добавлено %s',
- subscription.id,
- len(squads_to_remove),
- len(squads_to_add),
+ '♻️ Обновлены параметры подписки : удалено сквадов , добавлено',
+ subscription_id=subscription.id,
+ squads_to_remove_count=len(squads_to_remove),
+ squads_to_add_count=len(squads_to_add),
)
except Exception as error:
logger.error(
- '⚠️ Ошибка обновления счетчиков серверов при замене подписки %s: %s',
- subscription.id,
- error,
+ '⚠️ Ошибка обновления счетчиков серверов при замене подписки',
+ subscription_id=subscription.id,
+ error=error,
)
return subscription
@@ -351,9 +343,12 @@ async def extend_subscription(
"""
current_time = datetime.utcnow()
- logger.info(f'🔄 Продление подписки {subscription.id} на {days} дней')
+ logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days)
logger.info(
- f'📊 Текущие параметры: статус={subscription.status}, окончание={subscription.end_date}, тариф={subscription.tariff_id}'
+ '📊 Текущие параметры: статус=, окончание=, тариф',
+ status=subscription.status,
+ end_date=subscription.end_date,
+ tariff_id=subscription.tariff_id,
)
# Определяем, происходит ли СМЕНА тарифа (а не продление того же)
@@ -361,7 +356,7 @@ async def extend_subscription(
is_tariff_change = tariff_id is not None and (subscription.tariff_id is None or tariff_id != subscription.tariff_id)
if is_tariff_change:
- logger.info(f'🔄 Обнаружена СМЕНА тарифа: {subscription.tariff_id} → {tariff_id}')
+ logger.info('🔄 Обнаружена СМЕНА тарифа: →', tariff_id=subscription.tariff_id, tariff_id_2=tariff_id)
# Бонусные дни от триала - добавляются ТОЛЬКО когда подписка истекла
# и мы начинаем отсчёт с текущей даты. НЕ начисляются при смене тарифа.
@@ -371,9 +366,7 @@ async def extend_subscription(
if days < 0:
subscription.end_date = subscription.end_date + timedelta(days=days)
logger.info(
- '📅 Срок подписки уменьшен на %s дней, новая дата окончания: %s',
- abs(days),
- subscription.end_date,
+ '📅 Срок подписки уменьшен на дней, новая дата окончания', abs=abs(days), end_date=subscription.end_date
)
elif is_tariff_change:
# При СМЕНЕ тарифа срок начинается с текущей даты + бонус от триала
@@ -383,19 +376,19 @@ async def extend_subscription(
if remaining.total_seconds() > 0:
bonus_days = max(0, remaining.days)
logger.info(
- '🎁 Обнаружен остаток триала: %s дней для подписки %s',
- bonus_days,
- subscription.id,
+ '🎁 Обнаружен остаток триала: дней для подписки',
+ bonus_days=bonus_days,
+ subscription_id=subscription.id,
)
total_days = days + bonus_days
subscription.end_date = current_time + timedelta(days=total_days)
subscription.start_date = current_time
- logger.info(f'📅 СМЕНА тарифа: срок начинается с текущей даты + {total_days} дней')
+ logger.info('📅 СМЕНА тарифа: срок начинается с текущей даты + дней', total_days=total_days)
elif subscription.end_date > current_time:
# Подписка активна - просто добавляем дни к текущей дате окончания
# БЕЗ бонусных дней (они уже учтены в end_date)
subscription.end_date = subscription.end_date + timedelta(days=days)
- logger.info(f'📅 Подписка активна, добавляем {days} дней к текущей дате окончания')
+ logger.info('📅 Подписка активна, добавляем дней к текущей дате окончания', days=days)
else:
# Подписка истекла - начинаем с текущей даты + бонус от триала
if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID:
@@ -403,14 +396,19 @@ async def extend_subscription(
pass
total_days = days + bonus_days
subscription.end_date = current_time + timedelta(days=total_days)
- logger.info(f'📅 Подписка истекла, устанавливаем новую дату окончания на {total_days} дней')
+ logger.info('📅 Подписка истекла, устанавливаем новую дату окончания на дней', total_days=total_days)
# УДАЛЕНО: Автоматическая конвертация триала по длительности
# Теперь триал конвертируется ТОЛЬКО после успешного коммита продления
# и ТОЛЬКО вызывающей функцией (например, _auto_extend_subscription)
# Логируем статус подписки перед проверкой
- logger.info(f'🔄 Продление подписки {subscription.id}, текущий статус: {subscription.status}, дни: {days}')
+ logger.info(
+ '🔄 Продление подписки текущий статус: дни',
+ subscription_id=subscription.id,
+ status=subscription.status,
+ days=days,
+ )
if days > 0 and subscription.status in (
SubscriptionStatus.EXPIRED.value,
@@ -419,23 +417,21 @@ async def extend_subscription(
previous_status = subscription.status
subscription.status = SubscriptionStatus.ACTIVE.value
logger.info(
- '🔄 Статус подписки %s изменён с %s на ACTIVE',
- subscription.id,
- previous_status,
+ '🔄 Статус подписки изменён с на ACTIVE', subscription_id=subscription.id, previous_status=previous_status
)
elif days > 0 and subscription.status == SubscriptionStatus.PENDING.value:
- logger.warning('⚠️ Попытка продлить PENDING подписку %s, дни: %s', subscription.id, days)
+ logger.warning('⚠️ Попытка продлить PENDING подписку , дни', subscription_id=subscription.id, days=days)
# Обновляем параметры тарифа, если переданы
if tariff_id is not None:
old_tariff_id = subscription.tariff_id
subscription.tariff_id = tariff_id
- logger.info(f'📦 Обновлен тариф подписки: {old_tariff_id} → {tariff_id}')
+ logger.info('📦 Обновлен тариф подписки: →', old_tariff_id=old_tariff_id, tariff_id=tariff_id)
# При покупке тарифа сбрасываем триальный статус
if subscription.is_trial:
subscription.is_trial = False
- logger.info(f'🎓 Подписка {subscription.id} конвертирована из триала в платную')
+ logger.info('🎓 Подписка конвертирована из триала в платную', subscription_id=subscription.id)
if traffic_limit_gb is not None:
old_traffic = subscription.traffic_limit_gb
@@ -452,14 +448,19 @@ async def extend_subscription(
subscription.purchased_traffic_gb = 0
subscription.traffic_reset_at = None
logger.info(
- f'📊 Обновлен лимит трафика: {old_traffic} ГБ → {traffic_limit_gb} ГБ (смена тарифа, докупки сброшены)'
+ '📊 Обновлен лимит трафика: ГБ → ГБ (смена тарифа, докупки сброшены)',
+ old_traffic=old_traffic,
+ traffic_limit_gb=traffic_limit_gb,
)
else:
# При ПРОДЛЕНИИ того же тарифа — сохраняем докупленный трафик
purchased = subscription.purchased_traffic_gb or 0
subscription.traffic_limit_gb = traffic_limit_gb + purchased
logger.info(
- f'📊 Обновлен лимит трафика: {old_traffic} ГБ → {traffic_limit_gb + purchased} ГБ (докупки сохранены: {purchased} ГБ)'
+ '📊 Обновлен лимит трафика: ГБ → ГБ (докупки сохранены: ГБ)',
+ old_traffic=old_traffic,
+ traffic_limit_gb=traffic_limit_gb + purchased,
+ purchased=purchased,
)
elif settings.RESET_TRAFFIC_ON_PAYMENT:
subscription.traffic_used_gb = 0.0
@@ -475,12 +476,12 @@ async def extend_subscription(
if device_limit is not None:
old_devices = subscription.device_limit
subscription.device_limit = device_limit
- logger.info(f'📱 Обновлен лимит устройств: {old_devices} → {device_limit}')
+ logger.info('📱 Обновлен лимит устройств: →', old_devices=old_devices, device_limit=device_limit)
if connected_squads is not None:
old_squads = subscription.connected_squads
subscription.connected_squads = connected_squads
- logger.info(f'🌍 Обновлены сквады: {old_squads} → {connected_squads}')
+ logger.info('🌍 Обновлены сквады: →', old_squads=old_squads, connected_squads=connected_squads)
# Обработка daily полей при смене тарифа
if is_tariff_change and tariff_id is not None:
@@ -514,7 +515,11 @@ async def extend_subscription(
subscription.traffic_limit_gb = fixed_limit
subscription.purchased_traffic_gb = 0
subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика
- logger.info(f'🔄 Сброс трафика при продлении (fixed_with_topup): {old_limit} ГБ → {fixed_limit} ГБ')
+ logger.info(
+ '🔄 Сброс трафика при продлении (fixed_with_topup): ГБ → ГБ',
+ old_limit=old_limit,
+ fixed_limit=fixed_limit,
+ )
subscription.updated_at = current_time
@@ -522,8 +527,8 @@ async def extend_subscription(
await db.refresh(subscription)
await clear_notifications(db, subscription.id)
- logger.info(f'✅ Подписка продлена до: {subscription.end_date}')
- logger.info(f'📊 Новые параметры: статус={subscription.status}, окончание={subscription.end_date}')
+ logger.info('✅ Подписка продлена до', end_date=subscription.end_date)
+ logger.info('📊 Новые параметры: статус=, окончание', status=subscription.status, end_date=subscription.end_date)
return subscription
@@ -570,7 +575,10 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription,
await db.refresh(subscription)
logger.info(
- f'📈 К подписке пользователя {subscription.user_id} добавлено {gb} ГБ трафика (истекает {new_expires_at.strftime("%d.%m.%Y")})'
+ '📈 К подписке пользователя добавлено ГБ трафика (истекает )',
+ user_id=subscription.user_id,
+ gb=gb,
+ new_expires_at=new_expires_at.strftime('%d.%m.%Y'),
)
return subscription
@@ -582,7 +590,7 @@ async def add_subscription_devices(db: AsyncSession, subscription: Subscription,
await db.commit()
await db.refresh(subscription)
- logger.info(f'📱 К подписке пользователя {subscription.user_id} добавлено {devices} устройств')
+ logger.info('📱 К подписке пользователя добавлено устройств', user_id=subscription.user_id, devices=devices)
return subscription
@@ -594,7 +602,7 @@ async def add_subscription_squad(db: AsyncSession, subscription: Subscription, s
await db.commit()
await db.refresh(subscription)
- logger.info(f'🌍 К подписке пользователя {subscription.user_id} добавлен сквад {squad_uuid}')
+ logger.info('🌍 К подписке пользователя добавлен сквад', user_id=subscription.user_id, squad_uuid=squad_uuid)
return subscription
@@ -609,7 +617,7 @@ async def remove_subscription_squad(db: AsyncSession, subscription: Subscription
await db.commit()
await db.refresh(subscription)
- logger.info(f'🚫 Из подписки пользователя {subscription.user_id} удален сквад {squad_uuid}')
+ logger.info('🚫 Из подписки пользователя удален сквад', user_id=subscription.user_id, squad_uuid=squad_uuid)
return subscription
@@ -639,11 +647,7 @@ async def decrement_subscription_server_counts(
ids_from_links = await get_subscription_server_ids(db, sub_id)
server_ids.update(ids_from_links)
except Exception as error:
- logger.error(
- '⚠️ Не удалось получить серверы подписки %s для уменьшения счетчика: %s',
- sub_id,
- error,
- )
+ logger.error('⚠️ Не удалось получить серверы подписки для уменьшения счетчика', sub_id=sub_id, error=error)
connected_squads = list(subscription.connected_squads or [])
if connected_squads:
@@ -653,11 +657,7 @@ async def decrement_subscription_server_counts(
squad_server_ids = await get_server_ids_by_uuids(db, connected_squads)
server_ids.update(squad_server_ids)
except Exception as error:
- logger.error(
- '⚠️ Не удалось сопоставить сквады подписки %s с серверами: %s',
- sub_id,
- error,
- )
+ logger.error('⚠️ Не удалось сопоставить сквады подписки с серверами', sub_id=sub_id, error=error)
if not server_ids:
return
@@ -670,16 +670,16 @@ async def decrement_subscription_server_counts(
await remove_user_from_servers(db, list(server_ids))
except StaleDataError:
logger.warning(
- '⚠️ Подписка %s уже удалена (StaleDataError), пропускаем декремент серверов %s',
- sub_id,
- list(server_ids),
+ '⚠️ Подписка уже удалена (StaleDataError), пропускаем декремент серверов',
+ sub_id=sub_id,
+ list=list(server_ids),
)
except Exception as error:
logger.error(
- '⚠️ Ошибка уменьшения счетчика пользователей серверов %s для подписки %s: %s',
- list(server_ids),
- sub_id,
- error,
+ '⚠️ Ошибка уменьшения счетчика пользователей серверов для подписки',
+ list=list(server_ids),
+ sub_id=sub_id,
+ error=error,
)
@@ -694,7 +694,7 @@ async def update_subscription_autopay(
await db.refresh(subscription)
status = 'включен' if enabled else 'выключен'
- logger.info(f'💳 Автоплатеж для подписки пользователя {subscription.user_id} {status}')
+ logger.info('💳 Автоплатеж для подписки пользователя', user_id=subscription.user_id, status=status)
return subscription
@@ -705,7 +705,7 @@ async def deactivate_subscription(db: AsyncSession, subscription: Subscription)
await db.commit()
await db.refresh(subscription)
- logger.info(f'❌ Подписка пользователя {subscription.user_id} деактивирована')
+ logger.info('❌ Подписка пользователя деактивирована', user_id=subscription.user_id)
return subscription
@@ -844,9 +844,9 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
renewals_count = conversion_stats.get('month_conversions', 0)
logger.info('📊 Статистика конверсии из таблицы conversions:')
- logger.info(f' Общее количество конверсий: {conversion_stats.get("total_conversions", 0)}')
- logger.info(f' Процент конверсии: {trial_to_paid_conversion}%')
- logger.info(f' Конверсий за месяц: {renewals_count}')
+ logger.info('Общее количество конверсий', get=conversion_stats.get('total_conversions', 0))
+ logger.info('Процент конверсии', trial_to_paid_conversion=trial_to_paid_conversion)
+ logger.info('Конверсий за месяц', renewals_count=renewals_count)
except ImportError:
logger.warning('⚠️ Таблица subscription_conversions не найдена, используем старую логику')
@@ -943,9 +943,7 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- 'Не удалось обновить счётчики серверов при сбросе триала %s: %s',
- subscription.id,
- error,
+ 'Не удалось обновить счётчики серверов при сбросе триала', subscription_id=subscription.id, error=error
)
subscription_ids = [subscription.id for subscription in subscriptions]
@@ -954,11 +952,7 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
try:
await db.execute(delete(SubscriptionServer).where(SubscriptionServer.subscription_id.in_(subscription_ids)))
except Exception as error: # pragma: no cover - defensive logging
- logger.error(
- 'Ошибка удаления серверных связей триалов %s: %s',
- subscription_ids,
- error,
- )
+ logger.error('Ошибка удаления серверных связей триалов', subscription_ids=subscription_ids, error=error)
raise
await db.execute(delete(Subscription).where(Subscription.id.in_(subscription_ids)))
@@ -967,10 +961,10 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
await db.commit()
except Exception as error: # pragma: no cover - defensive logging
await db.rollback()
- logger.error('Ошибка сохранения сброса триалов: %s', error)
+ logger.error('Ошибка сохранения сброса триалов', error=error)
raise
- logger.info('♻️ Сброшено триальных подписок: %s', reset_count)
+ logger.info('♻️ Сброшено триальных подписок', reset_count=reset_count)
return reset_count
@@ -1047,7 +1041,12 @@ async def add_subscription_servers(
await db.commit()
await db.refresh(subscription)
- logger.info(f'🌐 К подписке {subscription.id} добавлено {len(server_squad_ids)} серверов с ценами: {paid_prices}')
+ logger.info(
+ '🌐 К подписке добавлено серверов с ценами',
+ subscription_id=subscription.id,
+ server_squad_ids_count=len(server_squad_ids),
+ paid_prices=paid_prices,
+ )
return subscription
@@ -1084,7 +1083,7 @@ async def get_servers_monthly_prices(
user_promo_group = user.get_primary_promo_group()
user_promo_group_id = user_promo_group.id if user_promo_group else None
except Exception as e:
- logger.warning(f'Не удалось получить промогруппу пользователя: {e}')
+ logger.warning('Не удалось получить промогруппу пользователя', error=e)
for server_id in server_squad_ids:
# Загружаем сервер с промогруппами
@@ -1110,9 +1109,11 @@ async def get_servers_monthly_prices(
else:
# Сервер недоступен для промогруппы пользователя
logger.warning(
- f'⚠️ Сервер {server.display_name} (id={server_id}) недоступен для '
- f'промогруппы пользователя (promo_group_id={user_promo_group_id}), '
- f'allowed_promo_groups={[pg.id for pg in server.allowed_promo_groups] if server.allowed_promo_groups else []}'
+ '⚠️ Сервер (id=) недоступен для промогруппы пользователя (promo_group_id=), allowed_promo_groups',
+ display_name=server.display_name,
+ server_id=server_id,
+ user_promo_group_id=user_promo_group_id,
+ value=[pg.id for pg in server.allowed_promo_groups] if server.allowed_promo_groups else [],
)
prices.append(server.price_kopeks) # Всё равно берём реальную цену
@@ -1227,8 +1228,10 @@ async def calculate_subscription_total_cost(
],
}
- logger.debug(f'📊 Расчет стоимости подписки на {period_days} дней ({months_in_period} мес):')
- logger.debug(f' Базовый период: {base_price / 100}₽')
+ logger.debug(
+ '📊 Расчет стоимости подписки на дней ( мес)', period_days=period_days, months_in_period=months_in_period
+ )
+ logger.debug('Базовый период: ₽', base_price=base_price / 100)
if total_traffic_price > 0:
message = f' Трафик: {traffic_price_per_month / 100}₽/мес × {months_in_period} = {total_traffic_price / 100}₽'
if total_traffic_discount > 0:
@@ -1248,7 +1251,7 @@ async def calculate_subscription_total_cost(
if total_devices_discount > 0:
message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)'
logger.debug(message)
- logger.debug(f' ИТОГО: {total_cost / 100}₽')
+ logger.debug('ИТОГО: ₽', total_cost=total_cost / 100)
return total_cost, details
@@ -1300,11 +1303,11 @@ async def remove_subscription_servers(db: AsyncSession, subscription_id: int, se
)
await db.commit()
- logger.info(f'🗑️ Удалены серверы {server_squad_ids} из подписки {subscription_id}')
+ logger.info('🗑️ Удалены серверы из подписки', server_squad_ids=server_squad_ids, subscription_id=subscription_id)
return True
except Exception as e:
- logger.error(f'Ошибка удаления серверов из подписки: {e}')
+ logger.error('Ошибка удаления серверов из подписки', error=e)
await db.rollback()
return False
@@ -1395,8 +1398,13 @@ async def get_subscription_renewal_cost(
total_cost = base_price + total_servers_cost + total_traffic_cost + total_devices_cost
- logger.info(f'💰 Расчет продления подписки {subscription_id} на {period_days} дней ({months_in_period} мес):')
- logger.info(f' 📅 Период: {base_price / 100}₽')
+ logger.info(
+ '💰 Расчет продления подписки на дней ( мес)',
+ subscription_id=subscription_id,
+ period_days=period_days,
+ months_in_period=months_in_period,
+ )
+ logger.info('📅 Период: ₽', base_price=base_price / 100)
if total_servers_cost > 0:
message = f' 🌍 Серверы: {servers_price_per_month / 100}₽/мес × {months_in_period} = {total_servers_cost / 100}₽'
if total_servers_discount > 0:
@@ -1414,12 +1422,12 @@ async def get_subscription_renewal_cost(
if total_devices_discount > 0:
message += f' (скидка {devices_discount_percent}%: -{total_devices_discount / 100}₽)'
logger.info(message)
- logger.info(f' 💎 ИТОГО: {total_cost / 100}₽')
+ logger.info('💎 ИТОГО: ₽', total_cost=total_cost / 100)
return total_cost
except Exception as e:
- logger.error(f'Ошибка расчета стоимости продления: {e}')
+ logger.error('Ошибка расчета стоимости продления', error=e)
from app.config import PERIOD_PRICES
return PERIOD_PRICES.get(period_days, 0)
@@ -1508,7 +1516,7 @@ async def calculate_addon_cost_for_remaining_period(
)
logger.info(message)
- logger.info(f'💰 Итого доплата за {months_to_pay} мес: {total_cost / 100}₽')
+ logger.info('💰 Итого доплата за мес: ₽', months_to_pay=months_to_pay, total_cost=total_cost / 100)
return total_cost
@@ -1519,7 +1527,7 @@ async def expire_subscription(db: AsyncSession, subscription: Subscription) -> S
await db.commit()
await db.refresh(subscription)
- logger.info(f'⏰ Подписка пользователя {subscription.user_id} помечена как истёкшая')
+ logger.info('⏰ Подписка пользователя помечена как истёкшая', user_id=subscription.user_id)
return subscription
@@ -1527,28 +1535,30 @@ async def check_and_update_subscription_status(db: AsyncSession, subscription: S
current_time = datetime.utcnow()
logger.info(
- '🔍 Проверка статуса подписки %s, текущий статус: %s, дата окончания: %s, текущее время: %s',
- subscription.id,
- subscription.status,
- format_local_datetime(subscription.end_date),
- format_local_datetime(current_time),
+ '🔍 Проверка статуса подписки , текущий статус дата окончания текущее время',
+ subscription_id=subscription.id,
+ subscription_status=subscription.status,
+ format_local_datetime=format_local_datetime(subscription.end_date),
+ format_local_datetime_2=format_local_datetime(current_time),
)
# Для суточных тарифов с паузой не меняем статус на expired
# (время "заморожено" пока пользователь на паузе)
is_daily_paused = getattr(subscription, 'is_daily_paused', False)
if is_daily_paused:
- logger.info(f'⏸️ Суточная подписка {subscription.id} на паузе, пропускаем проверку истечения')
+ logger.info('⏸️ Суточная подписка на паузе, пропускаем проверку истечения', subscription_id=subscription.id)
return subscription
if subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date <= current_time:
# Детальное логирование для отладки проблемы с деактивацией
time_diff = current_time - subscription.end_date
logger.warning(
- f'⏰ DEACTIVATION: подписка {subscription.id} (user_id={subscription.user_id}) '
- f'деактивируется в check_and_update_subscription_status. '
- f'end_date={subscription.end_date}, current_time={current_time}, '
- f'просрочена на {time_diff}'
+ '⏰ DEACTIVATION: подписка (user_id=) деактивируется в check_and_update_subscription_status. end_date=, current_time=, просрочена на',
+ subscription_id=subscription.id,
+ user_id=subscription.user_id,
+ end_date=subscription.end_date,
+ current_time=current_time,
+ time_diff=time_diff,
)
subscription.status = SubscriptionStatus.EXPIRED.value
@@ -1557,9 +1567,9 @@ async def check_and_update_subscription_status(db: AsyncSession, subscription: S
await db.commit()
await db.refresh(subscription)
- logger.info(f"⏰ Статус подписки пользователя {subscription.user_id} изменен на 'expired'")
+ logger.info("⏰ Статус подписки пользователя изменен на 'expired'", user_id=subscription.user_id)
elif subscription.status == SubscriptionStatus.PENDING.value:
- logger.info(f'ℹ️ Проверка PENDING подписки {subscription.id}, статус остается без изменений')
+ logger.info('ℹ️ Проверка PENDING подписки статус остается без изменений', subscription_id=subscription.id)
return subscription
@@ -1614,7 +1624,7 @@ async def create_subscription_no_commit(
await db.flush()
# Не коммитим сразу, оставляем для пакетной обработки
- logger.info(f'✅ Подготовлена подписка для пользователя {user_id} (ожидает коммита)')
+ logger.info('✅ Подготовлена подписка для пользователя (ожидает коммита)', user_id=user_id)
return subscription
@@ -1662,7 +1672,7 @@ async def create_subscription(
await db.commit()
await db.refresh(subscription)
- logger.info(f'✅ Создана подписка для пользователя {user_id}')
+ logger.info('✅ Создана подписка для пользователя', user_id=user_id)
return subscription
@@ -1694,9 +1704,9 @@ async def create_pending_subscription(
and existing_subscription.end_date > current_time
):
logger.warning(
- '⚠️ Попытка создать pending %sподписку для активного пользователя %s. Возвращаем существующую запись.',
- trial_label,
- user_id,
+ '⚠️ Попытка создать pending подписку для активного пользователя . Возвращаем существующую запись.',
+ trial_label=trial_label,
+ user_id=user_id,
)
return existing_subscription
@@ -1714,11 +1724,11 @@ async def create_pending_subscription(
await db.refresh(existing_subscription)
logger.info(
- '♻️ Обновлена ожидающая %sподписка пользователя %s, ID: %s, метод оплаты: %s',
- trial_label,
- user_id,
- existing_subscription.id,
- payment_method,
+ '♻️ Обновлена ожидающая подписка пользователя , ID метод оплаты',
+ trial_label=trial_label,
+ user_id=user_id,
+ existing_subscription_id=existing_subscription.id,
+ payment_method=payment_method,
)
return existing_subscription
@@ -1740,11 +1750,11 @@ async def create_pending_subscription(
await db.refresh(subscription)
logger.info(
- '💳 Создана ожидающая %sподписка для пользователя %s, ID: %s, метод оплаты: %s',
- trial_label,
- user_id,
- subscription.id,
- payment_method,
+ '💳 Создана ожидающая подписка для пользователя , ID метод оплаты',
+ trial_label=trial_label,
+ user_id=user_id,
+ subscription_id=subscription.id,
+ payment_method=payment_method,
)
return subscription
@@ -1777,7 +1787,7 @@ async def create_pending_trial_subscription(
async def activate_pending_subscription(db: AsyncSession, user_id: int, period_days: int = None) -> Subscription | None:
"""Активирует pending подписку пользователя, меняя её статус на ACTIVE."""
- logger.info(f'Активация pending подписки: пользователь {user_id}, период {period_days} дней')
+ logger.info('Активация pending подписки: пользователь период дней', user_id=user_id, period_days=period_days)
# Находим pending подписку пользователя
result = await db.execute(
@@ -1788,11 +1798,14 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
pending_subscription = result.scalar_one_or_none()
if not pending_subscription:
- logger.warning(f'Не найдена pending подписка для пользователя {user_id}')
+ logger.warning('Не найдена pending подписка для пользователя', user_id=user_id)
return None
logger.info(
- f'Найдена pending подписка {pending_subscription.id} для пользователя {user_id}, статус: {pending_subscription.status}'
+ 'Найдена pending подписка для пользователя статус',
+ pending_subscription_id=pending_subscription.id,
+ user_id=user_id,
+ status=pending_subscription.status,
)
# Обновляем статус подписки на ACTIVE
@@ -1812,7 +1825,9 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
await db.commit()
await db.refresh(pending_subscription)
- logger.info(f'Подписка пользователя {user_id} активирована, ID: {pending_subscription.id}')
+ logger.info(
+ 'Подписка пользователя активирована, ID', user_id=user_id, pending_subscription_id=pending_subscription.id
+ )
return pending_subscription
@@ -1823,7 +1838,11 @@ async def activate_pending_trial_subscription(
user_id: int,
) -> Subscription | None:
"""Активирует pending триальную подписку по её ID после оплаты."""
- logger.info(f'Активация pending триальной подписки: subscription_id={subscription_id}, user_id={user_id}')
+ logger.info(
+ 'Активация pending триальной подписки: subscription_id=, user_id',
+ subscription_id=subscription_id,
+ user_id=user_id,
+ )
# Находим pending подписку по ID
result = await db.execute(
@@ -1839,10 +1858,16 @@ async def activate_pending_trial_subscription(
pending_subscription = result.scalar_one_or_none()
if not pending_subscription:
- logger.warning(f'Не найдена pending триальная подписка {subscription_id} для пользователя {user_id}')
+ logger.warning(
+ 'Не найдена pending триальная подписка для пользователя', subscription_id=subscription_id, user_id=user_id
+ )
return None
- logger.info(f'Найдена pending триальная подписка {pending_subscription.id}, статус: {pending_subscription.status}')
+ logger.info(
+ 'Найдена pending триальная подписка статус',
+ pending_subscription_id=pending_subscription.id,
+ status=pending_subscription.status,
+ )
# Обновляем статус подписки на ACTIVE
current_time = datetime.utcnow()
@@ -1865,7 +1890,11 @@ async def activate_pending_trial_subscription(
await db.commit()
await db.refresh(pending_subscription)
- logger.info(f'Триальная подписка {pending_subscription.id} активирована для пользователя {user_id}')
+ logger.info(
+ 'Триальная подписка активирована для пользователя',
+ pending_subscription_id=pending_subscription.id,
+ user_id=user_id,
+ )
return pending_subscription
@@ -1911,7 +1940,7 @@ async def get_daily_subscriptions_for_charge(db: AsyncSession) -> list[Subscript
result = await db.execute(query)
subscriptions = result.scalars().all()
- logger.info(f'🔍 Найдено {len(subscriptions)} суточных подписок для списания')
+ logger.info('🔍 Найдено суточных подписок для списания', subscriptions_count=len(subscriptions))
return list(subscriptions)
@@ -1948,7 +1977,7 @@ async def get_disabled_daily_subscriptions_for_resume(
result = await db.execute(query)
subscriptions = result.scalars().all()
- logger.info(f'🔍 Найдено {len(subscriptions)} DISABLED суточных подписок для возобновления')
+ logger.info('🔍 Найдено DISABLED суточных подписок для возобновления', subscriptions_count=len(subscriptions))
return list(subscriptions)
@@ -1959,14 +1988,18 @@ async def pause_daily_subscription(
) -> Subscription:
"""Приостанавливает суточную подписку (списание не будет происходить)."""
if not subscription.is_daily_tariff:
- logger.warning(f'Попытка приостановить не-суточную подписку {subscription.id}')
+ logger.warning('Попытка приостановить не-суточную подписку', subscription_id=subscription.id)
return subscription
subscription.is_daily_paused = True
await db.commit()
await db.refresh(subscription)
- logger.info(f'⏸️ Суточная подписка {subscription.id} приостановлена пользователем {subscription.user_id}')
+ logger.info(
+ '⏸️ Суточная подписка приостановлена пользователем',
+ subscription_id=subscription.id,
+ user_id=subscription.user_id,
+ )
return subscription
@@ -1977,7 +2010,7 @@ async def resume_daily_subscription(
) -> Subscription:
"""Возобновляет суточную подписку (списание продолжится)."""
if not subscription.is_daily_tariff:
- logger.warning(f'Попытка возобновить не-суточную подписку {subscription.id}')
+ logger.warning('Попытка возобновить не-суточную подписку', subscription_id=subscription.id)
return subscription
subscription.is_daily_paused = False
@@ -1989,12 +2022,18 @@ async def resume_daily_subscription(
# Обновляем время последнего списания для корректного расчёта следующего
subscription.last_daily_charge_at = datetime.utcnow()
subscription.end_date = datetime.utcnow() + timedelta(days=1)
- logger.info(f'✅ Суточная подписка {subscription.id} восстановлена из {previous_status} в ACTIVE')
+ logger.info(
+ '✅ Суточная подписка восстановлена из в ACTIVE',
+ subscription_id=subscription.id,
+ previous_status=previous_status,
+ )
await db.commit()
await db.refresh(subscription)
- logger.info(f'▶️ Суточная подписка {subscription.id} возобновлена пользователем {subscription.user_id}')
+ logger.info(
+ '▶️ Суточная подписка возобновлена пользователем', subscription_id=subscription.id, user_id=subscription.user_id
+ )
return subscription
@@ -2012,7 +2051,7 @@ async def update_daily_charge_time(
new_end_date = now + timedelta(days=1)
if subscription.end_date is None or subscription.end_date < new_end_date:
subscription.end_date = new_end_date
- logger.info(f'📅 Продлена подписка {subscription.id} до {new_end_date}')
+ logger.info('📅 Продлена подписка до', subscription_id=subscription.id, new_end_date=new_end_date)
await db.commit()
await db.refresh(subscription)
@@ -2033,7 +2072,9 @@ async def suspend_daily_subscription_insufficient_balance(
await db.refresh(subscription)
logger.info(
- f'⚠️ Суточная подписка {subscription.id} приостановлена: недостаточно средств (user_id={subscription.user_id})'
+ '⚠️ Суточная подписка приостановлена: недостаточно средств (user_id=)',
+ subscription_id=subscription.id,
+ user_id=subscription.user_id,
)
return subscription
diff --git a/app/database/crud/subscription_conversion.py b/app/database/crud/subscription_conversion.py
index 62d78743..187c7a92 100644
--- a/app/database/crud/subscription_conversion.py
+++ b/app/database/crud/subscription_conversion.py
@@ -1,13 +1,13 @@
-import logging
from datetime import datetime, timedelta
+import structlog
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import SubscriptionConversion, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_subscription_conversion(
@@ -32,7 +32,11 @@ async def create_subscription_conversion(
await db.refresh(conversion)
logger.info(
- f'✅ Создана запись о конверсии для пользователя {user_id}: {trial_duration_days} дн. → {first_paid_period_days} дн. за {first_payment_amount_kopeks / 100}₽'
+ '✅ Создана запись о конверсии для пользователя дн. → дн. за ₽',
+ user_id=user_id,
+ trial_duration_days=trial_duration_days,
+ first_paid_period_days=first_paid_period_days,
+ first_payment_amount_kopeks=first_payment_amount_kopeks / 100,
)
return conversion
@@ -85,9 +89,9 @@ async def get_conversion_statistics(db: AsyncSession) -> dict:
month_conversions = month_conversions_result.scalar() or 0
logger.info('📊 Статистика конверсий:')
- logger.info(f' Всего пользователей с подписками: {total_users_with_subscriptions}')
- logger.info(f' Оплативших подписку: {users_with_paid}')
- logger.info(f' Рассчитанная конверсия: {conversion_rate}%')
+ logger.info('Всего пользователей с подписками', total_users_with_subscriptions=total_users_with_subscriptions)
+ logger.info('Оплативших подписку', users_with_paid=users_with_paid)
+ logger.info('Рассчитанная конверсия', conversion_rate=conversion_rate)
return {
'total_conversions': total_conversions,
diff --git a/app/database/crud/tariff.py b/app/database/crud/tariff.py
index dfb97caa..8b8f493c 100644
--- a/app/database/crud/tariff.py
+++ b/app/database/crud/tariff.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -7,7 +6,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoGroup, Subscription, Tariff
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _normalize_period_prices(period_prices: dict[int, int] | None) -> dict[str, int]:
@@ -241,13 +240,13 @@ async def create_tariff(
await db.refresh(tariff)
logger.info(
- "Создан тариф '%s' (id=%s, tier=%s, traffic=%sGB, devices=%s, prices=%s)",
- tariff.name,
- tariff.id,
- tariff.tier_level,
- tariff.traffic_limit_gb,
- tariff.device_limit,
- normalized_prices,
+ "Создан тариф '' (id tier traffic=GB, devices prices=)",
+ tariff_name=tariff.name,
+ tariff_id=tariff.id,
+ tier_level=tariff.tier_level,
+ traffic_limit_gb=tariff.traffic_limit_gb,
+ device_limit=tariff.device_limit,
+ normalized_prices=normalized_prices,
)
return tariff
@@ -365,11 +364,7 @@ async def update_tariff(
await db.commit()
await db.refresh(tariff)
- logger.info(
- "Обновлен тариф '%s' (id=%s)",
- tariff.name,
- tariff.id,
- )
+ logger.info("Обновлен тариф '' (id=)", tariff_name=tariff.name, tariff_id=tariff.id)
return tariff
@@ -393,10 +388,10 @@ async def delete_tariff(db: AsyncSession, tariff: Tariff) -> bool:
await db.commit()
logger.info(
- "Удален тариф '%s' (id=%s), затронуто подписок: %s",
- tariff_name,
- tariff_id,
- affected_subscriptions,
+ "Удален тариф '' (id=), затронуто подписок",
+ tariff_name=tariff_name,
+ tariff_id=tariff_id,
+ affected_subscriptions=affected_subscriptions,
)
return True
@@ -486,7 +481,7 @@ async def reorder_tariffs(
for order, tariff_id in enumerate(tariff_order):
await db.execute(update(Tariff).where(Tariff.id == tariff_id).values(display_order=order))
- logger.info('Изменен порядок тарифов: %s', tariff_order)
+ logger.info('Изменен порядок тарифов', tariff_order=tariff_order)
async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
@@ -522,7 +517,8 @@ async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
# Тариф уже существует — НЕ перезаписываем настройки из конфига.
# Админ управляет тарифом через кабинет, синхронизация не нужна.
logger.info(
- "Дефолтный тариф 'Стандартный' (id=%s) уже существует, пропускаем sync из конфига", existing_tariff.id
+ "Дефолтный тариф 'Стандартный' (id=) уже существует, пропускаем sync из конфига",
+ existing_tariff_id=existing_tariff.id,
)
return existing_tariff
@@ -544,7 +540,7 @@ async def sync_default_tariff_from_config(db: AsyncSession) -> Tariff | None:
db.add(new_tariff)
await db.commit()
await db.refresh(new_tariff)
- logger.info("Создан дефолтный тариф 'Стандартный' из конфига: %s", period_prices)
+ logger.info("Создан дефолтный тариф 'Стандартный' из конфига", period_prices=period_prices)
return new_tariff
return None
@@ -574,7 +570,7 @@ async def load_period_prices_from_db(db: AsyncSession) -> None:
return
if not tariff.period_prices:
- logger.warning("Тариф '%s' (id=%s) найден, но period_prices пуст", tariff.name, tariff.id)
+ logger.warning("Тариф '' (id=) найден, но period_prices пуст", tariff_name=tariff.name, tariff_id=tariff.id)
return
# Преобразуем строковые ключи в int
@@ -588,10 +584,10 @@ async def load_period_prices_from_db(db: AsyncSession) -> None:
{f'{d}д': f'{p // 100}₽' for d, p in period_prices.items()},
)
else:
- logger.warning("Тариф '%s' не имеет активных периодов (все цены = 0)", tariff.name)
+ logger.warning("Тариф '' не имеет активных периодов (все цены = 0)", tariff_name=tariff.name)
except Exception as e:
- logger.error('Ошибка загрузки периодов из БД: %s', e)
+ logger.error('Ошибка загрузки периодов из БД', e=e)
async def ensure_tariffs_synced(db: AsyncSession) -> None:
@@ -604,4 +600,4 @@ async def ensure_tariffs_synced(db: AsyncSession) -> None:
# Загружаем периоды из БД в PERIOD_PRICES
await load_period_prices_from_db(db)
except Exception as e:
- logger.error('Ошибка синхронизации тарифов: %s', e)
+ logger.error('Ошибка синхронизации тарифов', e=e)
diff --git a/app/database/crud/ticket.py b/app/database/crud/ticket.py
index 34776407..3d821e07 100644
--- a/app/database/crud/ticket.py
+++ b/app/database/crud/ticket.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import and_, desc, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import SupportAuditLog, Ticket, TicketMessage, TicketStatus
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class TicketCRUD:
@@ -64,7 +64,7 @@ class TicketCRUD:
db=db,
)
except Exception as error:
- logger.warning('Failed to emit ticket.created event: %s', error)
+ logger.warning('Failed to emit ticket.created event', error=error)
return ticket
@@ -248,7 +248,7 @@ class TicketCRUD:
db=db,
)
except Exception as error:
- logger.warning('Failed to emit ticket.status_changed event: %s', error)
+ logger.warning('Failed to emit ticket.status_changed event', error=error)
return True
@@ -440,7 +440,7 @@ class TicketMessageCRUD:
db=db,
)
except Exception as error:
- logger.warning('Failed to emit ticket.message_added event: %s', error)
+ logger.warning('Failed to emit ticket.message_added event', error=error)
return message
diff --git a/app/database/crud/ticket_notification.py b/app/database/crud/ticket_notification.py
index 0f801e05..a76ba244 100644
--- a/app/database/crud/ticket_notification.py
+++ b/app/database/crud/ticket_notification.py
@@ -1,8 +1,8 @@
"""CRUD operations for TicketNotification."""
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import desc, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -10,7 +10,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import Ticket, TicketNotification
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class TicketNotificationCRUD:
diff --git a/app/database/crud/transaction.py b/app/database/crud/transaction.py
index 18d33250..85886cfc 100644
--- a/app/database/crud/transaction.py
+++ b/app/database/crud/transaction.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime, timedelta
+import structlog
from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -8,7 +8,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PaymentMethod, Transaction, TransactionType, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Реальные платёжные методы для подсчёта дохода
# Исключены: MANUAL (админские), BALANCE (оплата с баланса), NULL (колесо, промокоды, бонусы)
@@ -55,7 +55,12 @@ async def create_transaction(
await db.commit()
await db.refresh(transaction)
- logger.info(f'💳 Создана транзакция: {type.value} на {amount_kopeks / 100}₽ для пользователя {user_id}')
+ logger.info(
+ '💳 Создана транзакция: на ₽ для пользователя',
+ type_value=type.value,
+ amount_kopeks=amount_kopeks / 100,
+ user_id=user_id,
+ )
# Отправляем событие о транзакции
try:
@@ -77,7 +82,7 @@ async def create_transaction(
db=db,
)
except Exception as error:
- logger.warning('Failed to emit transaction event: %s', error)
+ logger.warning('Failed to emit transaction event', error=error)
try:
from app.services.promo_group_assignment import (
@@ -86,11 +91,7 @@ async def create_transaction(
await maybe_assign_promo_group_by_total_spent(db, user_id)
except Exception as exc:
- logger.debug(
- 'Не удалось проверить автовыдачу промогруппы для пользователя %s: %s',
- user_id,
- exc,
- )
+ logger.debug('Не удалось проверить автовыдачу промогруппы для пользователя', user_id=user_id, exc=exc)
if type == TransactionType.SUBSCRIPTION_PAYMENT:
try:
from app.services.referral_contest_service import referral_contest_service
@@ -101,11 +102,7 @@ async def create_transaction(
amount_kopeks,
)
except Exception as exc:
- logger.debug(
- 'Не удалось записать событие конкурса для пользователя %s: %s',
- user_id,
- exc,
- )
+ logger.debug('Не удалось записать событие конкурса для пользователя', user_id=user_id, exc=exc)
return transaction
@@ -171,7 +168,7 @@ async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Tr
await db.commit()
await db.refresh(transaction)
- logger.info(f'✅ Транзакция {transaction.id} завершена')
+ logger.info('✅ Транзакция завершена', transaction_id=transaction.id)
try:
from app.services.promo_group_assignment import (
@@ -181,9 +178,7 @@ async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Tr
await maybe_assign_promo_group_by_total_spent(db, transaction.user_id)
except Exception as exc:
logger.debug(
- 'Не удалось проверить автовыдачу промогруппы для пользователя %s: %s',
- transaction.user_id,
- exc,
+ 'Не удалось проверить автовыдачу промогруппы для пользователя', user_id=transaction.user_id, exc=exc
)
return transaction
@@ -392,7 +387,7 @@ async def check_tribute_payment_duplicate(
transaction = result.scalar_one_or_none()
if transaction:
- logger.info(f'🔍 Найден дубликат платежа в течение 24ч: {transaction.id}')
+ logger.info('🔍 Найден дубликат платежа в течение 24ч', transaction_id=transaction.id)
return transaction
@@ -408,7 +403,7 @@ async def create_unique_tribute_transaction(
timestamp = int(datetime.utcnow().timestamp())
external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}'
- logger.info(f'Создан уникальный external_id для избежания дубликатов: {external_id}')
+ logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id)
return await create_transaction(
db=db,
diff --git a/app/database/crud/user.py b/app/database/crud/user.py
index 0e73f5e3..1df68097 100644
--- a/app/database/crud/user.py
+++ b/app/database/crud/user.py
@@ -1,8 +1,8 @@
-import logging
import secrets
import string
from datetime import UTC, datetime, timedelta
+import structlog
from sqlalchemy import and_, case, func, nullslast, or_, select, text
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -25,7 +25,7 @@ from app.database.models import (
from app.utils.validators import sanitize_telegram_name
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _normalize_language_code(language: str | None, fallback: str = 'ru') -> str:
@@ -269,7 +269,11 @@ async def create_user_no_commit(
user.promo_group = default_group
# Не коммитим сразу, оставляем для пакетной обработки
- logger.info(f'✅ Подготовлен пользователь {telegram_id} с реферальным кодом {referral_code} (ожидает коммита)')
+ logger.info(
+ '✅ Подготовлен пользователь с реферальным кодом (ожидает коммита)',
+ telegram_id=telegram_id,
+ referral_code=referral_code,
+ )
return user
@@ -316,7 +320,9 @@ async def create_user(
await db.refresh(user)
user.promo_group = default_group
- logger.info(f'✅ Создан пользователь {telegram_id} с реферальным кодом {referral_code}')
+ logger.info(
+ '✅ Создан пользователь с реферальным кодом', telegram_id=telegram_id, referral_code=referral_code
+ )
# Отправляем событие о создании пользователя
try:
@@ -336,7 +342,7 @@ async def create_user(
db=db,
)
except Exception as error:
- logger.warning('Failed to emit user.created event: %s', error)
+ logger.warning('Failed to emit user.created event', error=error)
return user
@@ -349,11 +355,10 @@ async def create_user(
and attempt < attempts
):
logger.warning(
- '⚠️ Обнаружено несоответствие последовательности users_id_seq при создании пользователя %s. '
- 'Выполняем повторную синхронизацию (попытка %s/%s)',
- telegram_id,
- attempt,
- attempts,
+ '⚠️ Обнаружено несоответствие последовательности users_id_seq при создании пользователя . Выполняем повторную синхронизацию (попытка /)',
+ telegram_id=telegram_id,
+ attempt=attempt,
+ attempts=attempts,
)
await _sync_users_sequence(db)
continue
@@ -413,7 +418,11 @@ async def add_user_balance(
user_id_display = user.telegram_id or user.email or f'#{user.id}'
logger.info(
- f'💰 Баланс пользователя {user_id_display} изменен: {old_balance} → {user.balance_kopeks} (изменение: +{amount_kopeks})'
+ '💰 Баланс пользователя изменен: → (изменение: +)',
+ user_id_display=user_id_display,
+ old_balance=old_balance,
+ balance_kopeks=user.balance_kopeks,
+ amount_kopeks=amount_kopeks,
)
# Автоматическое возобновление приостановленной суточной подписки
@@ -436,8 +445,9 @@ async def add_user_balance(
if daily_price > 0 and user.balance_kopeks >= daily_price:
await resume_daily_subscription(db, subscription)
logger.info(
- f'✅ Автоматически возобновлена суточная подписка {subscription.id} '
- f'после пополнения баланса (user_id={user.id})'
+ '✅ Автоматически возобновлена суточная подписка после пополнения баланса (user_id=)',
+ subscription_id=subscription.id,
+ user_id=user.id,
)
# Синхронизируем с RemnaWave
try:
@@ -446,14 +456,14 @@ async def add_user_balance(
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
except Exception as sync_err:
- logger.warning(f'Не удалось синхронизировать с RemnaWave: {sync_err}')
+ logger.warning('Не удалось синхронизировать с RemnaWave', sync_err=sync_err)
except Exception as resume_err:
- logger.warning(f'Ошибка при попытке возобновить суточную подписку: {resume_err}')
+ logger.warning('Ошибка при попытке возобновить суточную подписку', resume_err=resume_err)
return True
except Exception as e:
- logger.error(f'Ошибка изменения баланса пользователя {user.id}: {e}')
+ logger.error('Ошибка изменения баланса пользователя', user_id=user.id, error=e)
await db.rollback()
return False
@@ -469,7 +479,7 @@ async def add_user_balance_by_id(
try:
user = await get_user_by_telegram_id(db, telegram_id)
if not user:
- logger.error(f'Пользователь с telegram_id {telegram_id} не найден')
+ logger.error('Пользователь с telegram_id не найден', telegram_id=telegram_id)
return False
return await add_user_balance(
@@ -482,7 +492,7 @@ async def add_user_balance_by_id(
)
except Exception as e:
- logger.error(f'Ошибка пополнения баланса пользователя {telegram_id}: {e}')
+ logger.error('Ошибка пополнения баланса пользователя', telegram_id=telegram_id, error=e)
return False
@@ -498,10 +508,10 @@ async def subtract_user_balance(
) -> bool:
user_id_display = user.telegram_id or user.email or f'#{user.id}'
logger.info('💸 ОТЛАДКА subtract_user_balance:')
- logger.info(f' 👤 User ID: {user.id} (ID: {user_id_display})')
- logger.info(f' 💰 Баланс до списания: {user.balance_kopeks} копеек')
- logger.info(f' 💸 Сумма к списанию: {amount_kopeks} копеек')
- logger.info(f' 📝 Описание: {description}')
+ logger.info('👤 User ID: (ID: )', user_id=user.id, user_id_display=user_id_display)
+ logger.info('💰 Баланс до списания: копеек', balance_kopeks=user.balance_kopeks)
+ logger.info('💸 Сумма к списанию: копеек', amount_kopeks=amount_kopeks)
+ logger.info('📝 Описание', description=description)
# Lock the user row to prevent concurrent balance race conditions
locked_result = await db.execute(select(User).where(User.id == user.id).with_for_update())
@@ -531,9 +541,7 @@ async def subtract_user_balance(
offer = await get_latest_claimed_offer_for_user(db, user.id, source)
except Exception as lookup_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to fetch latest claimed promo offer for user %s: %s',
- user.id,
- lookup_error,
+ 'Failed to fetch latest claimed promo offer for user', user_id=user.id, lookup_error=lookup_error
)
offer = None
@@ -592,23 +600,21 @@ async def subtract_user_balance(
)
except Exception as log_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to record promo offer consumption log for user %s: %s',
- user.id,
- log_error,
+ 'Failed to record promo offer consumption log for user', user_id=user.id, log_error=log_error
)
try:
await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to rollback session after promo offer consumption log failure: %s',
- rollback_error,
+ 'Failed to rollback session after promo offer consumption log failure',
+ rollback_error=rollback_error,
)
- logger.info(f' ✅ Средства списаны: {old_balance} → {user.balance_kopeks}')
+ logger.info('✅ Средства списаны: →', old_balance=old_balance, balance_kopeks=user.balance_kopeks)
return True
except Exception as e:
- logger.error(f' ❌ ОШИБКА СПИСАНИЯ: {e}')
+ logger.error('❌ ОШИБКА СПИСАНИЯ', error=e)
await db.rollback()
return False
@@ -643,9 +649,9 @@ async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
offer = await get_latest_claimed_offer_for_user(db, user.id, source)
except Exception as lookup_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to fetch latest claimed promo offer for user %s during expiration cleanup: %s',
- user.id,
- lookup_error,
+ 'Failed to fetch latest claimed promo offer for user during expiration cleanup',
+ user_id=user.id,
+ lookup_error=lookup_error,
)
offer = None
@@ -688,17 +694,12 @@ async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
details={'reason': 'offer_expired'},
)
except Exception as log_error: # pragma: no cover - defensive logging
- logger.warning(
- 'Failed to log promo offer expiration for user %s: %s',
- user_id,
- log_error,
- )
+ logger.warning('Failed to log promo offer expiration for user', user_id=user_id, log_error=log_error)
try:
await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to rollback session after promo offer expiration log failure: %s',
- rollback_error,
+ 'Failed to rollback session after promo offer expiration log failure', rollback_error=rollback_error
)
return len(users)
@@ -944,7 +945,7 @@ async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[Us
),
)
else:
- logger.warning('Неизвестный сегмент для промо: %s', segment)
+ logger.warning('Неизвестный сегмент для промо', segment=segment)
return []
result = await db.execute(query.order_by(User.id))
@@ -989,7 +990,7 @@ async def delete_user(db: AsyncSession, user: User) -> bool:
await db.commit()
user_id_display = user.telegram_id or user.email or f'#{user.id}'
- logger.info(f'🗑️ Пользователь {user_id_display} помечен как удаленный')
+ logger.info('🗑️ Пользователь помечен как удаленный', user_id_display=user_id_display)
return True
@@ -1104,7 +1105,7 @@ async def create_user_by_email(
await db.refresh(user)
user.promo_group = default_group
- logger.info(f'✅ Создан email-пользователь {email} с id={user.id}')
+ logger.info('✅ Создан email-пользователь с id', email=email, user_id=user.id)
# Emit event
try:
@@ -1123,7 +1124,7 @@ async def create_user_by_email(
db=db,
)
except Exception as error:
- logger.warning('Failed to emit user.created event: %s', error)
+ logger.warning('Failed to emit user.created event', error=error)
return user
@@ -1181,7 +1182,7 @@ async def set_email_change_pending(
await db.commit()
await db.refresh(user)
- logger.info(f'Email change pending for user {user.id}: {user.email} -> {new_email}')
+ logger.info('Email change pending for user', user_id=user.id, email=user.email, new_email=new_email)
return user
@@ -1235,7 +1236,7 @@ async def verify_and_apply_email_change(db: AsyncSession, user: User, code: str)
await db.commit()
await db.refresh(user)
- logger.info(f'Email changed for user {user.id}: {old_email} -> {new_email}')
+ logger.info('Email changed for user', user_id=user.id, old_email=old_email, new_email=new_email)
return True, 'Email changed successfully'
@@ -1253,7 +1254,7 @@ async def clear_email_change_pending(db: AsyncSession, user: User) -> None:
user.updated_at = datetime.utcnow()
await db.commit()
- logger.info(f'Email change cancelled for user {user.id}')
+ logger.info('Email change cancelled for user', user_id=user.id)
# --- OAuth provider functions ---
@@ -1286,7 +1287,7 @@ async def set_user_oauth_provider_id(db: AsyncSession, user: User, provider: str
value: str | int = int(provider_id) if provider == 'vk' else provider_id
setattr(user, column_name, value)
user.updated_at = datetime.now(UTC).replace(tzinfo=None)
- logger.info(f'Linked {provider} (id={provider_id}) to user {user.id}')
+ logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id)
async def create_user_by_oauth(
@@ -1332,7 +1333,9 @@ async def create_user_by_oauth(
await db.refresh(user)
user.promo_group = default_group
- logger.info(f'Created OAuth user via {provider} (provider_id={provider_id}) with id={user.id}')
+ logger.info(
+ 'Created OAuth user via (provider_id=) with id', provider=provider, provider_id=provider_id, user_id=user.id
+ )
try:
from app.services.event_emitter import event_emitter
@@ -1349,6 +1352,6 @@ async def create_user_by_oauth(
db=db,
)
except Exception as error:
- logger.warning('Failed to emit user.created event: %s', error)
+ logger.warning('Failed to emit user.created event', error=error)
return user
diff --git a/app/database/crud/user_message.py b/app/database/crud/user_message.py
index d238077e..c8f69d48 100644
--- a/app/database/crud/user_message.py
+++ b/app/database/crud/user_message.py
@@ -1,7 +1,7 @@
-import logging
import random
from datetime import datetime
+import structlog
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -9,7 +9,7 @@ from app.database.models import User, UserMessage
from app.utils.validators import sanitize_html, validate_html_tags
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_user_message(
@@ -36,7 +36,7 @@ async def create_user_message(
await db.commit()
await db.refresh(message)
- logger.info(f'✅ Создано сообщение ID {message.id} пользователем {created_by}')
+ logger.info('✅ Создано сообщение ID пользователем', message_id=message.id, created_by=created_by)
return message
@@ -116,7 +116,7 @@ async def update_user_message(
await db.commit()
await db.refresh(message)
- logger.info(f'📝 Обновлено сообщение ID {message_id}')
+ logger.info('📝 Обновлено сообщение ID', message_id=message_id)
return message
@@ -133,7 +133,7 @@ async def toggle_user_message_status(db: AsyncSession, message_id: int) -> UserM
await db.refresh(message)
status_text = 'активировано' if message.is_active else 'деактивировано'
- logger.info(f'🔄 Сообщение ID {message_id} {status_text}')
+ logger.info('🔄 Сообщение ID', message_id=message_id, status_text=status_text)
return message
@@ -147,7 +147,7 @@ async def delete_user_message(db: AsyncSession, message_id: int) -> bool:
await db.delete(message)
await db.commit()
- logger.info(f'🗑️ Удалено сообщение ID {message_id}')
+ logger.info('🗑️ Удалено сообщение ID', message_id=message_id)
return True
diff --git a/app/database/crud/user_promo_group.py b/app/database/crud/user_promo_group.py
index ead124e1..1e1e0361 100644
--- a/app/database/crud/user_promo_group.py
+++ b/app/database/crud/user_promo_group.py
@@ -1,8 +1,8 @@
"""CRUD операции для связи пользователей с промогруппами (Many-to-Many)."""
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -10,7 +10,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import PromoGroup, User, UserPromoGroup
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _sync_user_primary_promo_group(
@@ -39,11 +39,7 @@ async def _sync_user_primary_promo_group(
user.updated_at = datetime.utcnow()
except Exception as error:
- logger.error(
- 'Ошибка синхронизации primary промогруппы пользователя %s: %s',
- user_id,
- error,
- )
+ logger.error('Ошибка синхронизации primary промогруппы пользователя', user_id=user_id, error=error)
async def sync_user_primary_promo_group(
@@ -74,7 +70,7 @@ async def add_user_to_promo_group(
# Проверяем существование связи
existing = await has_user_promo_group(db, user_id, promo_group_id)
if existing:
- logger.info(f'Пользователь {user_id} уже имеет промогруппу {promo_group_id}')
+ logger.info('Пользователь уже имеет промогруппу', user_id=user_id, promo_group_id=promo_group_id)
return None
# Создаем новую связь
@@ -91,11 +87,16 @@ async def add_user_to_promo_group(
await db.commit()
await db.refresh(user_promo_group)
- logger.info(f'Пользователю {user_id} добавлена промогруппа {promo_group_id} ({assigned_by})')
+ logger.info(
+ 'Пользователю добавлена промогруппа',
+ user_id=user_id,
+ promo_group_id=promo_group_id,
+ assigned_by=assigned_by,
+ )
return user_promo_group
except Exception as error:
- logger.error(f'Ошибка добавления промогруппы пользователю: {error}')
+ logger.error('Ошибка добавления промогруппы пользователю', error=error)
await db.rollback()
return None
@@ -121,7 +122,9 @@ async def remove_user_from_promo_group(db: AsyncSession, user_id: int, promo_gro
user_promo_group = result.scalar_one_or_none()
if not user_promo_group:
- logger.warning(f'Связь пользователя {user_id} с промогруппой {promo_group_id} не найдена')
+ logger.warning(
+ 'Связь пользователя с промогруппой не найдена', user_id=user_id, promo_group_id=promo_group_id
+ )
return False
await db.delete(user_promo_group)
@@ -131,11 +134,11 @@ async def remove_user_from_promo_group(db: AsyncSession, user_id: int, promo_gro
await db.commit()
- logger.info(f'У пользователя {user_id} удалена промогруппа {promo_group_id}')
+ logger.info('У пользователя удалена промогруппа', user_id=user_id, promo_group_id=promo_group_id)
return True
except Exception as error:
- logger.error(f'Ошибка удаления промогруппы у пользователя: {error}')
+ logger.error('Ошибка удаления промогруппы у пользователя', error=error)
await db.rollback()
return False
@@ -162,7 +165,7 @@ async def get_user_promo_groups(db: AsyncSession, user_id: int) -> list[UserProm
return list(result.scalars().all())
except Exception as error:
- logger.error(f'Ошибка получения промогрупп пользователя {user_id}: {error}')
+ logger.error('Ошибка получения промогрупп пользователя', user_id=user_id, error=error)
return []
@@ -187,7 +190,7 @@ async def get_primary_user_promo_group(db: AsyncSession, user_id: int) -> PromoG
return user_promo_groups[0].promo_group if user_promo_groups[0].promo_group else None
except Exception as error:
- logger.error(f'Ошибка получения primary промогруппы пользователя {user_id}: {error}')
+ logger.error('Ошибка получения primary промогруппы пользователя', user_id=user_id, error=error)
return None
@@ -212,7 +215,7 @@ async def has_user_promo_group(db: AsyncSession, user_id: int, promo_group_id: i
return result.scalar_one_or_none() is not None
except Exception as error:
- logger.error(f'Ошибка проверки промогруппы пользователя: {error}')
+ logger.error('Ошибка проверки промогруппы пользователя', error=error)
return False
@@ -232,7 +235,7 @@ async def count_user_promo_groups(db: AsyncSession, user_id: int) -> int:
return len(list(result.scalars().all()))
except Exception as error:
- logger.error(f'Ошибка подсчета промогрупп пользователя: {error}')
+ logger.error('Ошибка подсчета промогрупп пользователя', error=error)
return 0
@@ -264,10 +267,10 @@ async def replace_user_promo_groups(
db.add(user_promo_group)
await db.commit()
- logger.info(f'Промогруппы пользователя {user_id} заменены на {promo_group_ids}')
+ logger.info('Промогруппы пользователя заменены на', user_id=user_id, promo_group_ids=promo_group_ids)
return True
except Exception as error:
- logger.error(f'Ошибка замены промогрупп пользователя: {error}')
+ logger.error('Ошибка замены промогрупп пользователя', error=error)
await db.rollback()
return False
diff --git a/app/database/crud/wata.py b/app/database/crud/wata.py
index b4e3df34..8cf2bf31 100644
--- a/app/database/crud/wata.py
+++ b/app/database/crud/wata.py
@@ -1,16 +1,16 @@
"""CRUD helpers for WATA payment records."""
-import logging
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import WataPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_wata_payment(
@@ -53,11 +53,11 @@ async def create_wata_payment(
await db.refresh(payment)
logger.info(
- 'Создан Wata платеж #%s для пользователя %s: %s копеек (статус %s)',
- payment.id,
- user_id,
- amount_kopeks,
- status,
+ 'Создан Wata платеж # для пользователя : копеек (статус)',
+ payment_id=payment.id,
+ user_id=user_id,
+ amount_kopeks=amount_kopeks,
+ status=status,
)
return payment
@@ -128,10 +128,10 @@ async def update_wata_payment_status(
await db.refresh(payment)
logger.info(
- 'Обновлен Wata платеж %s: статус=%s, is_paid=%s',
- payment.payment_link_id,
- payment.status,
- payment.is_paid,
+ 'Обновлен Wata платеж : статус is_paid',
+ payment_link_id=payment.payment_link_id,
+ payment_status=payment.status,
+ is_paid=payment.is_paid,
)
return payment
@@ -147,9 +147,7 @@ async def link_wata_payment_to_transaction(
await db.refresh(payment)
logger.info(
- 'Wata платеж %s привязан к транзакции %s',
- payment.payment_link_id,
- transaction_id,
+ 'Wata платеж привязан к транзакции', payment_link_id=payment.payment_link_id, transaction_id=transaction_id
)
return payment
diff --git a/app/database/crud/welcome_text.py b/app/database/crud/welcome_text.py
index 6588219a..b48df45e 100644
--- a/app/database/crud/welcome_text.py
+++ b/app/database/crud/welcome_text.py
@@ -1,13 +1,13 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User, WelcomeText
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
WELCOME_TEXT_KEY = 'welcome_text'
@@ -83,7 +83,7 @@ async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
await db.refresh(welcome_text)
status = 'включен' if welcome_text.is_enabled else 'отключен'
- logger.info(f'Приветственный текст {status} администратором {admin_id}')
+ logger.info('Приветственный текст администратором', status=status, admin_id=admin_id)
return welcome_text.is_enabled
default_text = await get_current_welcome_text_or_default()
new_welcome_text = WelcomeText(text_content=default_text, is_active=True, is_enabled=True, created_by=admin_id)
@@ -92,11 +92,11 @@ async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
await db.commit()
await db.refresh(new_welcome_text)
- logger.info(f'Создан и включен дефолтный приветственный текст администратором {admin_id}')
+ logger.info('Создан и включен дефолтный приветственный текст администратором', admin_id=admin_id)
return True
except Exception as e:
- logger.error(f'Ошибка при переключении статуса приветственного текста: {e}')
+ logger.error('Ошибка при переключении статуса приветственного текста', error=e)
await db.rollback()
return False
@@ -116,11 +116,11 @@ async def set_welcome_text(db: AsyncSession, text_content: str, admin_id: int) -
await db.commit()
await db.refresh(new_welcome_text)
- logger.info(f'Установлен новый приветственный текст администратором {admin_id}')
+ logger.info('Установлен новый приветственный текст администратором', admin_id=admin_id)
return True
except Exception as e:
- logger.error(f'Ошибка при установке приветственного текста: {e}')
+ logger.error('Ошибка при установке приветственного текста', error=e)
await db.rollback()
return False
@@ -154,10 +154,10 @@ async def create_welcome_text(
await db.refresh(welcome_text)
logger.info(
- '✅ Создан приветственный текст ID %s (активный=%s, включен=%s)',
- welcome_text.id,
- welcome_text.is_active,
- welcome_text.is_enabled,
+ '✅ Создан приветственный текст ID (активный включен=)',
+ welcome_text_id=welcome_text.id,
+ is_active=welcome_text.is_active,
+ is_enabled=welcome_text.is_enabled,
)
return welcome_text
@@ -188,10 +188,10 @@ async def update_welcome_text(
await db.refresh(welcome_text)
logger.info(
- '📝 Обновлен приветственный текст ID %s (активный=%s, включен=%s)',
- welcome_text.id,
- welcome_text.is_active,
- welcome_text.is_enabled,
+ '📝 Обновлен приветственный текст ID (активный включен=)',
+ welcome_text_id=welcome_text.id,
+ is_active=welcome_text.is_active,
+ is_enabled=welcome_text.is_enabled,
)
return welcome_text
@@ -199,7 +199,7 @@ async def update_welcome_text(
async def delete_welcome_text(db: AsyncSession, welcome_text: WelcomeText) -> None:
await db.delete(welcome_text)
await db.commit()
- logger.info('🗑️ Удален приветственный текст ID %s', welcome_text.id)
+ logger.info('🗑️ Удален приветственный текст ID', welcome_text_id=welcome_text.id)
async def get_current_welcome_text_or_default() -> str:
diff --git a/app/database/crud/wheel.py b/app/database/crud/wheel.py
index 0e617653..a494a075 100644
--- a/app/database/crud/wheel.py
+++ b/app/database/crud/wheel.py
@@ -2,10 +2,10 @@
CRUD операции для колеса удачи (Fortune Wheel).
"""
-import logging
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy import and_, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -17,7 +17,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# ==================== WHEEL CONFIG ====================
@@ -68,7 +68,7 @@ async def update_wheel_config(db: AsyncSession, **kwargs) -> WheelConfig:
config.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(config)
- logger.info(f'🎡 Обновлена конфигурация колеса: {kwargs}')
+ logger.info('🎡 Обновлена конфигурация колеса', kwargs=kwargs)
return config
@@ -129,7 +129,7 @@ async def create_wheel_prize(
db.add(prize)
await db.commit()
await db.refresh(prize)
- logger.info(f'🎁 Создан приз колеса: {display_name} ({prize_type})')
+ logger.info('🎁 Создан приз колеса', display_name=display_name, prize_type=prize_type)
return prize
@@ -146,7 +146,7 @@ async def update_wheel_prize(db: AsyncSession, prize_id: int, **kwargs) -> Wheel
prize.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(prize)
- logger.info(f'🎁 Обновлен приз колеса ID={prize_id}: {kwargs}')
+ logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs)
return prize
@@ -158,7 +158,7 @@ async def delete_wheel_prize(db: AsyncSession, prize_id: int) -> bool:
await db.delete(prize)
await db.commit()
- logger.info(f'🗑️ Удален приз колеса ID={prize_id}')
+ logger.info('🗑️ Удален приз колеса ID', prize_id=prize_id)
return True
@@ -170,7 +170,7 @@ async def reorder_wheel_prizes(db: AsyncSession, prize_ids: list[int]) -> bool:
prize.sort_order = index
await db.commit()
- logger.info(f'🔄 Переупорядочены призы колеса: {prize_ids}')
+ logger.info('🔄 Переупорядочены призы колеса', prize_ids=prize_ids)
return True
@@ -209,7 +209,7 @@ async def create_wheel_spin(
db.add(spin)
await db.commit()
await db.refresh(spin)
- logger.info(f"🎰 Создан спин колеса: user_id={user_id}, prize='{prize_display_name}'")
+ logger.info('🎰 Создан спин колеса: user_id=, prize', user_id=user_id, prize_display_name=prize_display_name)
return spin
diff --git a/app/database/crud/yookassa.py b/app/database/crud/yookassa.py
index 0fc265b0..05153833 100644
--- a/app/database/crud/yookassa.py
+++ b/app/database/crud/yookassa.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import and_, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -9,7 +9,7 @@ from sqlalchemy.orm import selectinload
from app.database.models import YooKassaPayment
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def create_yookassa_payment(
@@ -46,15 +46,20 @@ async def create_yookassa_payment(
except IntegrityError as e:
await db.rollback()
logger.error(
- 'FK violation при создании платежа YooKassa %s: user_id=%s не существует в БД: %s',
- yookassa_payment_id,
- user_id,
- e,
+ 'FK violation при создании платежа YooKassa : user_id= не существует в БД',
+ yookassa_payment_id=yookassa_payment_id,
+ user_id=user_id,
+ e=e,
)
return None
await db.refresh(payment)
- logger.info(f'Создан платеж YooKassa: {yookassa_payment_id} на {amount_kopeks / 100}₽ для пользователя {user_id}')
+ logger.info(
+ 'Создан платеж YooKassa: на ₽ для пользователя',
+ yookassa_payment_id=yookassa_payment_id,
+ amount_kopeks=amount_kopeks / 100,
+ user_id=user_id,
+ )
return payment
@@ -104,7 +109,12 @@ async def update_yookassa_payment_status(
payment = result.scalar_one_or_none()
if payment:
- logger.info(f'Обновлен статус платежа YooKassa {yookassa_payment_id}: {status}, paid={is_paid}')
+ logger.info(
+ 'Обновлен статус платежа YooKassa , paid',
+ yookassa_payment_id=yookassa_payment_id,
+ status=status,
+ is_paid=is_paid,
+ )
return payment
@@ -127,7 +137,11 @@ async def link_yookassa_payment_to_transaction(
payment = result.scalar_one_or_none()
if payment:
- logger.info(f'Платеж YooKassa {yookassa_payment_id} связан с транзакцией {transaction_id}')
+ logger.info(
+ 'Платеж YooKassa связан с транзакцией',
+ yookassa_payment_id=yookassa_payment_id,
+ transaction_id=transaction_id,
+ )
return payment
@@ -185,7 +199,7 @@ async def delete_yookassa_payment(db: AsyncSession, yookassa_payment_id: str) ->
if payment:
await db.delete(payment)
await db.commit()
- logger.info(f'Удален платеж YooKassa: {yookassa_payment_id}')
+ logger.info('Удален платеж YooKassa', yookassa_payment_id=yookassa_payment_id)
return True
return False
diff --git a/app/database/database.py b/app/database/database.py
index 65348cea..d8bdc592 100644
--- a/app/database/database.py
+++ b/app/database/database.py
@@ -1,11 +1,11 @@
import asyncio
-import logging
import time
from collections.abc import AsyncGenerator, Callable
from contextlib import asynccontextmanager
from functools import wraps
from typing import TypeVar
+import structlog
from sqlalchemy import bindparam, event, inspect, text
from sqlalchemy.engine import Engine
from sqlalchemy.exc import InterfaceError, OperationalError, ProgrammingError
@@ -16,7 +16,7 @@ from app.config import settings
from app.database.models import Base
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
T = TypeVar('T')
@@ -126,16 +126,16 @@ def with_db_retry(
last_exception = e
if attempt < attempts:
logger.warning(
- 'Ошибка БД (попытка %d/%d): %s. Повтор через %.1f сек...',
- attempt,
- attempts,
- str(e)[:100],
- current_delay,
+ 'Ошибка БД (попытка /): . Повтор через сек...',
+ attempt=attempt,
+ attempts=attempts,
+ e=str(e)[:100],
+ current_delay=current_delay,
)
await asyncio.sleep(current_delay)
current_delay *= backoff
else:
- logger.error('Ошибка БД: все %d попыток исчерпаны. Последняя ошибка: %s', attempts, str(e))
+ logger.error('Ошибка БД: все попыток исчерпаны. Последняя ошибка', attempts=attempts, e=str(e))
raise last_exception
@@ -159,7 +159,7 @@ async def execute_with_retry(
except RETRYABLE_EXCEPTIONS as e:
last_exception = e
if attempt < attempts:
- logger.warning('SQL retry (попытка %d/%d): %s', attempt, attempts, str(e)[:100])
+ logger.warning('SQL retry (попытка /)', attempt=attempt, attempts=attempts, e=str(e)[:100])
await asyncio.sleep(delay)
delay *= 2
@@ -175,15 +175,15 @@ if settings.DEBUG:
@event.listens_for(Engine, 'before_cursor_execute')
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
conn.info.setdefault('query_start_time', []).append(time.time())
- logger.debug(f'🔍 Executing query: {statement[:100]}...')
+ logger.debug('🔍 Executing query: ...', statement=statement[:100])
@event.listens_for(Engine, 'after_cursor_execute')
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
total = time.time() - conn.info['query_start_time'].pop(-1)
if total > 0.1: # Логируем медленные запросы > 100ms
- logger.warning(f'🐌 Slow query ({total:.3f}s): {statement[:100]}...')
+ logger.warning('🐌 Slow query (s): ...', total=round(total, 3), statement=statement[:100])
else:
- logger.debug(f'⚡ Query executed in {total:.3f}s')
+ logger.debug('⚡ Query executed in', total=round(total, 3))
# ============================================================================
# ADVANCED SESSION MANAGER WITH READ REPLICAS
@@ -201,7 +201,7 @@ def _validate_database_url(url: str | None) -> str | None:
return None
# Простая проверка на валидный формат
if not ('://' in url or url.startswith('sqlite')):
- logger.warning('Невалидный DATABASE_URL: %s', url[:20])
+ logger.warning('Невалидный DATABASE_URL', url=url[:20])
return None
return url
@@ -234,9 +234,9 @@ class DatabaseManager:
expire_on_commit=False,
autoflush=False,
)
- logger.info('Read replica настроена: %s', replica_url[:30] + '...')
+ logger.info('Read replica настроена', replica_url=replica_url[:30] + '...')
except Exception as e:
- logger.error('Не удалось настроить read replica: %s', e)
+ logger.error('Не удалось настроить read replica', e=e)
self.read_replica_engine = None
@asynccontextmanager
@@ -276,10 +276,10 @@ class DatabaseManager:
latency = (time.time() - start) * 1000
status = 'healthy'
except TimeoutError:
- logger.error('Health check таймаут (%s сек)', timeout)
+ logger.error('Health check таймаут (сек)', timeout=timeout)
status = 'timeout'
except Exception as e:
- logger.error('Database health check failed: %s', e)
+ logger.error('Database health check failed', e=e)
status = 'unhealthy'
return {
@@ -307,7 +307,7 @@ class DatabaseManager:
except TimeoutError:
status = 'timeout'
except Exception as e:
- logger.error('Read replica health check failed: %s', e)
+ logger.error('Read replica health check failed', e=e)
return {
'status': status,
@@ -440,7 +440,7 @@ async def init_db():
# Продолжаем выполнение, так как основные таблицы могут быть созданы
else:
# Для других ошибок пробрасываем исключение
- logger.error(f'❌ Ошибка при создании таблиц: {e}')
+ logger.error('❌ Ошибка при создании таблиц', error=e)
raise
if not IS_SQLITE:
@@ -468,21 +468,19 @@ async def init_db():
if not table_exists:
logger.debug(
- 'Пропускаем создание индекса %s: таблица %s отсутствует',
- index_sql,
- table_name,
+ 'Пропускаем создание индекса : таблица отсутствует', index_sql=index_sql, table_name=table_name
)
continue
try:
await conn.execute(text(index_sql))
except Exception as e:
- logger.debug('Index creation skipped for %s: %s', table_name, e)
+ logger.debug('Index creation skipped for', table_name=table_name, e=e)
logger.info('База данных успешно инициализирована')
health = await db_manager.health_check()
- logger.info('Database health: %s', health)
+ logger.info('Database health', health=health)
async def close_db():
diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py
index abde8182..85ed3da7 100644
--- a/app/database/universal_migration.py
+++ b/app/database/universal_migration.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from sqlalchemy import select, text
from app.config import settings
@@ -9,7 +9,7 @@ from app.database.models import WebApiToken
from app.utils.security import hash_api_token
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_database_type():
@@ -22,7 +22,7 @@ async def sync_postgres_sequences() -> bool:
db_type = await get_database_type()
if db_type != 'postgresql':
- logger.debug('Пропускаем синхронизацию последовательностей: тип БД %s', db_type)
+ logger.debug('Пропускаем синхронизацию последовательностей: тип БД', db_type=db_type)
return True
try:
@@ -88,16 +88,16 @@ async def sync_postgres_sequences() -> bool:
{'sequence_name': sequence_path, 'new_value': max_value},
)
logger.info(
- '🔄 Последовательность %s синхронизирована: MAX=%s, следующий ID=%s',
- sequence_path,
- max_value,
- max_value + 1,
+ '🔄 Последовательность синхронизирована: MAX следующий ID',
+ sequence_path=sequence_path,
+ max_value=max_value,
+ max_value_2=max_value + 1,
)
return True
except Exception as error:
- logger.error('❌ Ошибка синхронизации последовательностей PostgreSQL: %s', error)
+ logger.error('❌ Ошибка синхронизации последовательностей PostgreSQL', error=error)
return False
@@ -138,7 +138,7 @@ async def check_table_exists(table_name: str) -> bool:
return False
except Exception as e:
- logger.error(f'Ошибка проверки существования таблицы {table_name}: {e}')
+ logger.error('Ошибка проверки существования таблицы', table_name=table_name, error=e)
return False
@@ -179,7 +179,7 @@ async def check_column_exists(table_name: str, column_name: str) -> bool:
return False
except Exception as e:
- logger.error(f'Ошибка проверки существования колонки {column_name}: {e}')
+ logger.error('Ошибка проверки существования колонки', column_name=column_name, error=e)
return False
@@ -226,7 +226,12 @@ async def check_constraint_exists(table_name: str, constraint_name: str) -> bool
return False
except Exception as e:
- logger.error(f'Ошибка проверки существования ограничения {constraint_name} для {table_name}: {e}')
+ logger.error(
+ 'Ошибка проверки существования ограничения для',
+ constraint_name=constraint_name,
+ table_name=table_name,
+ error=e,
+ )
return False
@@ -273,7 +278,7 @@ async def check_index_exists(table_name: str, index_name: str) -> bool:
return False
except Exception as e:
- logger.error(f'Ошибка проверки существования индекса {index_name} для {table_name}: {e}')
+ logger.error('Ошибка проверки существования индекса для', index_name=index_name, table_name=table_name, error=e)
return False
@@ -443,11 +448,11 @@ async def enforce_wata_payment_link_constraints(
return unique_index_exists, legacy_index_exists
- logger.warning('⚠️ Неизвестный тип БД %s — не удалось усилить ограничения payment_link_id', db_type)
+ logger.warning('⚠️ Неизвестный тип БД — не удалось усилить ограничения payment_link_id', db_type=db_type)
return unique_index_exists, legacy_index_exists
except Exception as e:
- logger.error(f'Ошибка настройки ограничений payment_link_id: {e}')
+ logger.error('Ошибка настройки ограничений payment_link_id', error=e)
return unique_index_exists, legacy_index_exists
@@ -542,7 +547,7 @@ async def create_cryptobot_payments_table():
CREATE INDEX idx_cryptobot_payments_status ON cryptobot_payments(status);
"""
else:
- logger.error(f'Неподдерживаемый тип БД для создания таблицы: {db_type}')
+ logger.error('Неподдерживаемый тип БД для создания таблицы', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -550,7 +555,7 @@ async def create_cryptobot_payments_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы cryptobot_payments: {e}')
+ logger.error('Ошибка создания таблицы cryptobot_payments', error=e)
return False
@@ -656,7 +661,7 @@ async def create_heleket_payments_table():
"""
else:
- logger.error(f'Неподдерживаемый тип БД для таблицы heleket_payments: {db_type}')
+ logger.error('Неподдерживаемый тип БД для таблицы heleket_payments', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -664,7 +669,7 @@ async def create_heleket_payments_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы heleket_payments: {e}')
+ logger.error('Ошибка создания таблицы heleket_payments', error=e)
return False
@@ -758,7 +763,7 @@ async def create_mulenpay_payments_table():
"""
else:
- logger.error(f'Неподдерживаемый тип БД для таблицы mulenpay_payments: {db_type}')
+ logger.error('Неподдерживаемый тип БД для таблицы mulenpay_payments', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -766,7 +771,7 @@ async def create_mulenpay_payments_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы mulenpay_payments: {e}')
+ logger.error('Ошибка создания таблицы mulenpay_payments', error=e)
return False
@@ -793,8 +798,7 @@ async def ensure_mulenpay_payment_schema() -> bool:
alter_sql = 'ALTER TABLE mulenpay_payments ADD COLUMN mulen_payment_id INT NULL'
else:
logger.error(
- 'Неподдерживаемый тип БД для добавления mulen_payment_id в mulenpay_payments: %s',
- db_type,
+ 'Неподдерживаемый тип БД для добавления mulen_payment_id в mulenpay_payments', db_type=db_type
)
return False
@@ -811,10 +815,7 @@ async def ensure_mulenpay_payment_schema() -> bool:
elif db_type == 'mysql':
alter_paid_at_sql = 'ALTER TABLE mulenpay_payments ADD COLUMN paid_at DATETIME NULL'
else:
- logger.error(
- 'Неподдерживаемый тип БД для добавления paid_at в mulenpay_payments: %s',
- db_type,
- )
+ logger.error('Неподдерживаемый тип БД для добавления paid_at в mulenpay_payments', db_type=db_type)
return False
await conn.execute(text(alter_paid_at_sql))
@@ -830,10 +831,7 @@ async def ensure_mulenpay_payment_schema() -> bool:
elif db_type == 'mysql':
create_index_sql = 'CREATE INDEX idx_mulenpay_payment_id ON mulenpay_payments(mulen_payment_id)'
else:
- logger.error(
- 'Неподдерживаемый тип БД для создания индекса mulenpay_payment_id: %s',
- db_type,
- )
+ logger.error('Неподдерживаемый тип БД для создания индекса mulenpay_payment_id', db_type=db_type)
return False
await conn.execute(text(create_index_sql))
@@ -844,7 +842,7 @@ async def ensure_mulenpay_payment_schema() -> bool:
return True
except Exception as e:
- logger.error(f'Ошибка обновления схемы mulenpay_payments: {e}')
+ logger.error('Ошибка обновления схемы mulenpay_payments', error=e)
return False
@@ -980,7 +978,7 @@ async def create_pal24_payments_table():
"""
else:
- logger.error(f'Неподдерживаемый тип БД для таблицы pal24_payments: {db_type}')
+ logger.error('Неподдерживаемый тип БД для таблицы pal24_payments', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -988,7 +986,7 @@ async def create_pal24_payments_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы pal24_payments: {e}')
+ logger.error('Ошибка создания таблицы pal24_payments', error=e)
return False
@@ -1100,7 +1098,7 @@ async def create_wata_payments_table():
"""
else:
- logger.error(f'Неподдерживаемый тип БД для таблицы wata_payments: {db_type}')
+ logger.error('Неподдерживаемый тип БД для таблицы wata_payments', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -1108,7 +1106,7 @@ async def create_wata_payments_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы wata_payments: {e}')
+ logger.error('Ошибка создания таблицы wata_payments', error=e)
return False
@@ -1153,10 +1151,7 @@ async def ensure_wata_payment_schema() -> bool:
await conn.execute(text('ALTER TABLE wata_payments ADD COLUMN payment_link_id VARCHAR(64)'))
payment_link_column_exists = True
else:
- logger.warning(
- '⚠️ Неизвестный тип БД %s — пропущено добавление payment_link_id',
- db_type,
- )
+ logger.warning('⚠️ Неизвестный тип БД — пропущено добавление payment_link_id', db_type=db_type)
if payment_link_column_exists:
logger.info('✅ Добавлена колонка payment_link_id в wata_payments')
@@ -1180,10 +1175,7 @@ async def ensure_wata_payment_schema() -> bool:
await conn.execute(text('ALTER TABLE wata_payments ADD COLUMN order_id VARCHAR(255)'))
order_id_column_exists = True
else:
- logger.warning(
- '⚠️ Неизвестный тип БД %s — пропущено добавление order_id',
- db_type,
- )
+ logger.warning('⚠️ Неизвестный тип БД — пропущено добавление order_id', db_type=db_type)
if order_id_column_exists:
logger.info('✅ Добавлена колонка order_id в wata_payments')
@@ -1203,8 +1195,7 @@ async def ensure_wata_payment_schema() -> bool:
index_created = True
else:
logger.warning(
- '⚠️ Неизвестный тип БД %s — пропущено создание индекса idx_wata_order_id',
- db_type,
+ '⚠️ Неизвестный тип БД — пропущено создание индекса idx_wata_order_id', db_type=db_type
)
if index_created:
@@ -1215,7 +1206,7 @@ async def ensure_wata_payment_schema() -> bool:
return True
except Exception as e:
- logger.error(f'Ошибка обновления схемы wata_payments: {e}')
+ logger.error('Ошибка обновления схемы wata_payments', error=e)
return False
@@ -1319,7 +1310,7 @@ async def create_freekassa_payments_table():
"""
else:
- logger.error(f'Неподдерживаемый тип БД для таблицы freekassa_payments: {db_type}')
+ logger.error('Неподдерживаемый тип БД для таблицы freekassa_payments', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -1327,7 +1318,7 @@ async def create_freekassa_payments_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы freekassa_payments: {e}')
+ logger.error('Ошибка создания таблицы freekassa_payments', error=e)
return False
@@ -1431,7 +1422,7 @@ async def create_kassa_ai_payments_table():
"""
else:
- logger.error(f'Неподдерживаемый тип БД для таблицы kassa_ai_payments: {db_type}')
+ logger.error('Неподдерживаемый тип БД для таблицы kassa_ai_payments', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -1439,7 +1430,7 @@ async def create_kassa_ai_payments_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы kassa_ai_payments: {e}')
+ logger.error('Ошибка создания таблицы kassa_ai_payments', error=e)
return False
@@ -1545,7 +1536,7 @@ async def create_discount_offers_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы discount_offers: {e}')
+ logger.error('Ошибка создания таблицы discount_offers', error=e)
return False
@@ -1638,7 +1629,7 @@ async def create_referral_contests_table() -> bool:
logger.info('✅ Таблица referral_contests создана')
return True
except Exception as error:
- logger.error(f'Ошибка создания таблицы referral_contests: {error}')
+ logger.error('Ошибка создания таблицы referral_contests', error=error)
return False
@@ -1727,7 +1718,7 @@ async def create_referral_contest_events_table() -> bool:
logger.info('✅ Таблица referral_contest_events создана')
return True
except Exception as error:
- logger.error(f'Ошибка создания таблицы referral_contest_events: {error}')
+ logger.error('Ошибка создания таблицы referral_contest_events', error=error)
return False
@@ -1786,7 +1777,7 @@ async def create_referral_contest_virtual_participants_table() -> bool:
logger.info('✅ Таблица referral_contest_virtual_participants создана')
return True
except Exception as error:
- logger.error(f'Ошибка создания таблицы referral_contest_virtual_participants: {error}')
+ logger.error('Ошибка создания таблицы referral_contest_virtual_participants', error=error)
return False
@@ -1795,7 +1786,7 @@ async def ensure_referral_contest_summary_columns() -> bool:
for column in ['daily_summary_times', 'last_daily_summary_at']:
exists = await check_column_exists('referral_contests', column)
if exists:
- logger.info('Колонка %s в referral_contests уже существует', column)
+ logger.info('Колонка в referral_contests уже существует', column=column)
continue
try:
async with engine.begin() as conn:
@@ -1814,10 +1805,10 @@ async def ensure_referral_contest_summary_columns() -> bool:
+ ('VARCHAR(255)' if column == 'daily_summary_times' else 'DATETIME')
)
)
- logger.info('✅ Колонка %s в referral_contests добавлена', column)
+ logger.info('✅ Колонка в referral_contests добавлена', column=column)
except Exception as error:
ok = False
- logger.error('Ошибка добавления %s в referral_contests: %s', column, error)
+ logger.error('Ошибка добавления в referral_contests', column=column, error=error)
return ok
@@ -1900,7 +1891,7 @@ async def create_contest_templates_table() -> bool:
logger.info('✅ Таблица contest_templates создана')
return True
except Exception as error:
- logger.error(f'Ошибка создания таблицы contest_templates: {error}')
+ logger.error('Ошибка создания таблицы contest_templates', error=error)
return False
@@ -1996,7 +1987,7 @@ async def create_contest_rounds_table() -> bool:
logger.info('✅ Таблица contest_rounds создана')
return True
except Exception as error:
- logger.error(f'Ошибка создания таблицы contest_rounds: {error}')
+ logger.error('Ошибка создания таблицы contest_rounds', error=error)
return False
@@ -2069,7 +2060,7 @@ async def create_contest_attempts_table() -> bool:
logger.info('✅ Таблица contest_attempts создана')
return True
except Exception as error:
- logger.error(f'Ошибка создания таблицы contest_attempts: {error}')
+ logger.error('Ошибка создания таблицы contest_attempts', error=error)
return False
@@ -2096,7 +2087,7 @@ async def ensure_referral_contest_type_column() -> bool:
logger.info('✅ Колонка contest_type в referral_contests добавлена')
return True
except Exception as error:
- logger.error(f'Ошибка добавления contest_type в referral_contests: {error}')
+ logger.error('Ошибка добавления contest_type в referral_contests', error=error)
return False
@@ -2133,7 +2124,7 @@ async def ensure_discount_offer_columns():
return True
except Exception as e:
- logger.error(f'Ошибка обновления колонок discount_offers: {e}')
+ logger.error('Ошибка обновления колонок discount_offers', error=e)
return False
@@ -2180,7 +2171,7 @@ async def ensure_user_promo_offer_discount_columns():
logger.info('✅ Колонки promo_offer_discount_* для users проверены')
return True
except Exception as e:
- logger.error(f'Ошибка обновления колонок promo_offer_discount_*: {e}')
+ logger.error('Ошибка обновления колонок promo_offer_discount_*', error=e)
return False
@@ -2209,7 +2200,7 @@ async def ensure_user_notification_settings_column() -> bool:
logger.info('✅ Колонка notification_settings для users добавлена')
return True
except Exception as e:
- logger.error(f'Ошибка добавления колонки notification_settings: {e}')
+ logger.error('Ошибка добавления колонки notification_settings', error=e)
return False
@@ -2244,7 +2235,7 @@ async def ensure_promo_offer_template_active_duration_column() -> bool:
logger.info('✅ Колонка active_discount_hours в promo_offer_templates актуальна')
return True
except Exception as e:
- logger.error(f'Ошибка обновления active_discount_hours в promo_offer_templates: {e}')
+ logger.error('Ошибка обновления active_discount_hours в promo_offer_templates', error=e)
return False
@@ -2257,7 +2248,7 @@ async def migrate_discount_offer_effect_types():
logger.info('✅ Типы эффектов discount_offers обновлены на percent_discount')
return True
except Exception as e:
- logger.error(f'Ошибка обновления типов эффектов discount_offers: {e}')
+ logger.error('Ошибка обновления типов эффектов discount_offers', error=e)
return False
@@ -2273,7 +2264,7 @@ async def reset_discount_offer_bonuses():
logger.info('✅ Бонусы промо-предложений сброшены до нуля')
return True
except Exception as e:
- logger.error(f'Ошибка обнуления бонусов промо-предложений: {e}')
+ logger.error('Ошибка обнуления бонусов промо-предложений', error=e)
return False
@@ -2364,7 +2355,7 @@ async def create_promo_offer_templates_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы promo_offer_templates: {e}')
+ logger.error('Ошибка создания таблицы promo_offer_templates', error=e)
return False
@@ -2427,7 +2418,7 @@ async def create_main_menu_buttons_table() -> bool:
CREATE INDEX ix_main_menu_buttons_order ON main_menu_buttons(display_order, id);
"""
else:
- logger.error(f'Неподдерживаемый тип БД для таблицы main_menu_buttons: {db_type}')
+ logger.error('Неподдерживаемый тип БД для таблицы main_menu_buttons', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -2436,7 +2427,7 @@ async def create_main_menu_buttons_table() -> bool:
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы main_menu_buttons: {e}')
+ logger.error('Ошибка создания таблицы main_menu_buttons', error=e)
return False
@@ -2509,13 +2500,13 @@ async def create_promo_offer_logs_table() -> bool:
""")
)
else:
- logger.warning('Неизвестный тип БД для создания promo_offer_logs: %s', db_type)
+ logger.warning('Неизвестный тип БД для создания promo_offer_logs', db_type=db_type)
return False
logger.info('✅ Таблица promo_offer_logs успешно создана')
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы promo_offer_logs: {e}')
+ logger.error('Ошибка создания таблицы promo_offer_logs', error=e)
return False
@@ -2596,7 +2587,7 @@ async def create_subscription_temporary_access_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы subscription_temporary_access: {e}')
+ logger.error('Ошибка создания таблицы subscription_temporary_access', error=e)
return False
@@ -2661,7 +2652,7 @@ async def create_user_messages_table():
CREATE INDEX idx_user_messages_sort ON user_messages(sort_order, created_at);
"""
else:
- logger.error(f'Неподдерживаемый тип БД для создания таблицы: {db_type}')
+ logger.error('Неподдерживаемый тип БД для создания таблицы', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -2669,7 +2660,7 @@ async def create_user_messages_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы user_messages: {e}')
+ logger.error('Ошибка создания таблицы user_messages', error=e)
return False
@@ -2740,7 +2731,7 @@ async def ensure_promo_groups_setup():
)
)
else:
- logger.error(f'Неподдерживаемый тип БД для promo_groups: {db_type}')
+ logger.error('Неподдерживаемый тип БД для promo_groups', db_type=db_type)
return False
logger.info('Создана таблица promo_groups')
@@ -2751,7 +2742,7 @@ async def ensure_promo_groups_setup():
text('ALTER TABLE promo_groups ADD CONSTRAINT uq_promo_groups_name UNIQUE (name)')
)
except Exception as e:
- logger.warning(f'Не удалось добавить уникальное ограничение uq_promo_groups_name: {e}')
+ logger.warning('Не удалось добавить уникальное ограничение uq_promo_groups_name', error=e)
period_discounts_column_exists = await check_column_exists('promo_groups', 'period_discounts')
@@ -2772,7 +2763,7 @@ async def ensure_promo_groups_setup():
text('UPDATE promo_groups SET period_discounts = JSON_OBJECT() WHERE period_discounts IS NULL')
)
else:
- logger.error(f'Неподдерживаемый тип БД для promo_groups.period_discounts: {db_type}')
+ logger.error('Неподдерживаемый тип БД для promo_groups.period_discounts', db_type=db_type)
return False
logger.info('Добавлена колонка promo_groups.period_discounts')
@@ -2789,7 +2780,9 @@ async def ensure_promo_groups_setup():
text('ALTER TABLE promo_groups ADD COLUMN auto_assign_total_spent_kopeks INT DEFAULT 0')
)
else:
- logger.error(f'Неподдерживаемый тип БД для promo_groups.auto_assign_total_spent_kopeks: {db_type}')
+ logger.error(
+ 'Неподдерживаемый тип БД для promo_groups.auto_assign_total_spent_kopeks', db_type=db_type
+ )
return False
logger.info('Добавлена колонка promo_groups.auto_assign_total_spent_kopeks')
@@ -2830,7 +2823,7 @@ async def ensure_promo_groups_setup():
)
)
else:
- logger.error(f'Неподдерживаемый тип БД для promo_groups.apply_discounts_to_addons: {db_type}')
+ logger.error('Неподдерживаемый тип БД для promo_groups.apply_discounts_to_addons', db_type=db_type)
return False
logger.info('Добавлена колонка promo_groups.apply_discounts_to_addons')
@@ -2844,7 +2837,7 @@ async def ensure_promo_groups_setup():
elif db_type == 'mysql':
await conn.execute(text('ALTER TABLE users ADD COLUMN promo_group_id INT'))
else:
- logger.error(f'Неподдерживаемый тип БД для promo_group_id: {db_type}')
+ logger.error('Неподдерживаемый тип БД для promo_group_id', db_type=db_type)
return False
logger.info('Добавлена колонка users.promo_group_id')
@@ -2863,7 +2856,7 @@ async def ensure_promo_groups_setup():
text('ALTER TABLE users ADD COLUMN auto_promo_group_assigned TINYINT(1) DEFAULT 0')
)
else:
- logger.error(f'Неподдерживаемый тип БД для users.auto_promo_group_assigned: {db_type}')
+ logger.error('Неподдерживаемый тип БД для users.auto_promo_group_assigned', db_type=db_type)
return False
logger.info('Добавлена колонка users.auto_promo_group_assigned')
@@ -2882,7 +2875,7 @@ async def ensure_promo_groups_setup():
text('ALTER TABLE users ADD COLUMN auto_promo_group_threshold_kopeks BIGINT NOT NULL DEFAULT 0')
)
else:
- logger.error(f'Неподдерживаемый тип БД для users.auto_promo_group_threshold_kopeks: {db_type}')
+ logger.error('Неподдерживаемый тип БД для users.auto_promo_group_threshold_kopeks', db_type=db_type)
return False
logger.info('Добавлена колонка users.auto_promo_group_threshold_kopeks')
@@ -2899,7 +2892,7 @@ async def ensure_promo_groups_setup():
await conn.execute(text('CREATE INDEX ix_users_promo_group_id ON users(promo_group_id)'))
logger.info('Создан индекс ix_users_promo_group_id')
except Exception as e:
- logger.warning(f'Не удалось создать индекс ix_users_promo_group_id: {e}')
+ logger.warning('Не удалось создать индекс ix_users_promo_group_id', error=e)
default_group_name = 'Базовый юзер'
default_group_id = None
@@ -3023,12 +3016,12 @@ async def ensure_promo_groups_setup():
)
logger.info('Добавлен внешний ключ users -> promo_groups')
except Exception as e:
- logger.warning(f'Не удалось добавить внешний ключ users.promo_group_id: {e}')
+ logger.warning('Не удалось добавить внешний ключ users.promo_group_id', error=e)
try:
await conn.execute(text('ALTER TABLE users ALTER COLUMN promo_group_id SET NOT NULL'))
except Exception as e:
- logger.warning(f'Не удалось сделать users.promo_group_id NOT NULL: {e}')
+ logger.warning('Не удалось сделать users.promo_group_id NOT NULL', error=e)
elif db_type == 'mysql':
constraint_exists = await check_constraint_exists('users', 'fk_users_promo_group_id_promo_groups')
@@ -3047,18 +3040,18 @@ async def ensure_promo_groups_setup():
)
logger.info('Добавлен внешний ключ users -> promo_groups')
except Exception as e:
- logger.warning(f'Не удалось добавить внешний ключ users.promo_group_id: {e}')
+ logger.warning('Не удалось добавить внешний ключ users.promo_group_id', error=e)
try:
await conn.execute(text('ALTER TABLE users MODIFY promo_group_id INT NOT NULL'))
except Exception as e:
- logger.warning(f'Не удалось сделать users.promo_group_id NOT NULL: {e}')
+ logger.warning('Не удалось сделать users.promo_group_id NOT NULL', error=e)
logger.info('✅ Промо группы настроены')
return True
except Exception as e:
- logger.error(f'Ошибка настройки промо групп: {e}')
+ logger.error('Ошибка настройки промо групп', error=e)
return False
@@ -3077,7 +3070,7 @@ async def add_welcome_text_is_enabled_column():
elif db_type == 'postgresql' or db_type == 'mysql':
alter_sql = 'ALTER TABLE welcome_texts ADD COLUMN is_enabled BOOLEAN DEFAULT TRUE NOT NULL'
else:
- logger.error(f'Неподдерживаемый тип БД для добавления колонки: {db_type}')
+ logger.error('Неподдерживаемый тип БД для добавления колонки', db_type=db_type)
return False
await conn.execute(text(alter_sql))
@@ -3090,12 +3083,12 @@ async def add_welcome_text_is_enabled_column():
result = await conn.execute(text(update_sql))
updated_count = result.rowcount
- logger.info(f'Обновлено {updated_count} существующих записей welcome_texts')
+ logger.info('Обновлено существующих записей welcome_texts', updated_count=updated_count)
return True
except Exception as e:
- logger.error(f'Ошибка при добавлении поля is_enabled: {e}')
+ logger.error('Ошибка при добавлении поля is_enabled', error=e)
return False
@@ -3163,7 +3156,7 @@ async def create_welcome_texts_table():
CREATE INDEX idx_welcome_texts_updated ON welcome_texts(updated_at);
"""
else:
- logger.error(f'Неподдерживаемый тип БД для создания таблицы: {db_type}')
+ logger.error('Неподдерживаемый тип БД для создания таблицы', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -3171,7 +3164,7 @@ async def create_welcome_texts_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы welcome_texts: {e}')
+ logger.error('Ошибка создания таблицы welcome_texts', error=e)
return False
@@ -3242,7 +3235,7 @@ async def create_pinned_messages_table():
"""
else:
- logger.error(f'Неподдерживаемый тип БД для создания таблицы pinned_messages: {db_type}')
+ logger.error('Неподдерживаемый тип БД для создания таблицы pinned_messages', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -3251,7 +3244,7 @@ async def create_pinned_messages_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы pinned_messages: {e}')
+ logger.error('Ошибка создания таблицы pinned_messages', error=e)
return False
@@ -3300,7 +3293,7 @@ async def ensure_pinned_message_media_columns():
return True
except Exception as e:
- logger.error(f'Ошибка обновления медиа полей pinned_messages: {e}')
+ logger.error('Ошибка обновления медиа полей pinned_messages', error=e)
return False
@@ -3312,7 +3305,7 @@ async def ensure_user_last_pinned_column():
logger.info('✅ Поле last_pinned_message_id у пользователей готово')
return True
except Exception as e:
- logger.error(f'Ошибка добавления поля last_pinned_message_id: {e}')
+ logger.error('Ошибка добавления поля last_pinned_message_id', error=e)
return False
@@ -3334,7 +3327,7 @@ async def add_media_fields_to_broadcast_history():
field_exists = await check_column_exists('broadcast_history', field_name)
if not field_exists:
- logger.info(f'Добавление поля {field_name} в таблицу broadcast_history')
+ logger.info('Добавление поля в таблицу broadcast_history', field_name=field_name)
if db_type == 'sqlite':
if 'BOOLEAN' in field_type:
@@ -3345,15 +3338,15 @@ async def add_media_fields_to_broadcast_history():
alter_sql = f'ALTER TABLE broadcast_history ADD COLUMN {field_name} {field_type}'
await conn.execute(text(alter_sql))
- logger.info(f'✅ Поле {field_name} успешно добавлено')
+ logger.info('✅ Поле успешно добавлено', field_name=field_name)
else:
- logger.info(f'Поле {field_name} уже существует в broadcast_history')
+ logger.info('Поле уже существует в broadcast_history', field_name=field_name)
logger.info('✅ Все поля медиа в broadcast_history готовы')
return True
except Exception as e:
- logger.error(f'Ошибка при добавлении полей медиа в broadcast_history: {e}')
+ logger.error('Ошибка при добавлении полей медиа в broadcast_history', error=e)
return False
@@ -3376,13 +3369,13 @@ async def add_email_fields_to_broadcast_history():
field_exists = await check_column_exists('broadcast_history', field_name)
if not field_exists:
- logger.info(f'Добавление поля {field_name} в таблицу broadcast_history')
+ logger.info('Добавление поля в таблицу broadcast_history', field_name=field_name)
alter_sql = f'ALTER TABLE broadcast_history ADD COLUMN {field_name} {field_type}'
await conn.execute(text(alter_sql))
- logger.info(f'✅ Поле {field_name} успешно добавлено')
+ logger.info('✅ Поле успешно добавлено', field_name=field_name)
else:
- logger.info(f'Поле {field_name} уже существует в broadcast_history')
+ logger.info('Поле уже существует в broadcast_history', field_name=field_name)
# Сделать message_text nullable для email-only рассылок
try:
@@ -3395,13 +3388,13 @@ async def add_email_fields_to_broadcast_history():
# SQLite не поддерживает ALTER COLUMN, но там по умолчанию nullable
except Exception as e:
# Игнорируем если уже nullable или другая ошибка
- logger.debug(f'message_text nullable: {e}')
+ logger.debug('message_text nullable', error=e)
logger.info('✅ Все поля email в broadcast_history готовы')
return True
except Exception as e:
- logger.error(f'Ошибка при добавлении полей email в broadcast_history: {e}')
+ logger.error('Ошибка при добавлении полей email в broadcast_history', error=e)
return False
@@ -3424,7 +3417,7 @@ async def add_ticket_reply_block_columns():
'ALTER TABLE tickets ADD COLUMN user_reply_block_permanent BOOLEAN DEFAULT FALSE NOT NULL'
)
else:
- logger.error(f'Неподдерживаемый тип БД для добавления user_reply_block_permanent: {db_type}')
+ logger.error('Неподдерживаемый тип БД для добавления user_reply_block_permanent', db_type=db_type)
return False
await conn.execute(text(alter_sql))
logger.info('✅ Добавлена колонка tickets.user_reply_block_permanent')
@@ -3437,14 +3430,14 @@ async def add_ticket_reply_block_columns():
elif db_type == 'mysql':
alter_sql = 'ALTER TABLE tickets ADD COLUMN user_reply_block_until DATETIME NULL'
else:
- logger.error(f'Неподдерживаемый тип БД для добавления user_reply_block_until: {db_type}')
+ logger.error('Неподдерживаемый тип БД для добавления user_reply_block_until', db_type=db_type)
return False
await conn.execute(text(alter_sql))
logger.info('✅ Добавлена колонка tickets.user_reply_block_until')
return True
except Exception as e:
- logger.error(f'Ошибка добавления колонок блокировок в tickets: {e}')
+ logger.error('Ошибка добавления колонок блокировок в tickets', error=e)
return False
@@ -3462,13 +3455,13 @@ async def add_ticket_sla_columns():
elif db_type == 'mysql':
alter_sql = 'ALTER TABLE tickets ADD COLUMN last_sla_reminder_at DATETIME NULL'
else:
- logger.error(f'Неподдерживаемый тип БД для добавления last_sla_reminder_at: {db_type}')
+ logger.error('Неподдерживаемый тип БД для добавления last_sla_reminder_at', db_type=db_type)
return False
await conn.execute(text(alter_sql))
logger.info('✅ Добавлена колонка tickets.last_sla_reminder_at')
return True
except Exception as e:
- logger.error(f'Ошибка добавления SLA колонки в tickets: {e}')
+ logger.error('Ошибка добавления SLA колонки в tickets', error=e)
return False
@@ -3496,7 +3489,7 @@ async def add_user_restriction_columns() -> bool:
text('ALTER TABLE users ADD COLUMN restriction_topup BOOLEAN DEFAULT FALSE NOT NULL')
)
else:
- logger.error(f'Неподдерживаемый тип БД: {db_type}')
+ logger.error('Неподдерживаемый тип БД', db_type=db_type)
return False
logger.info('✅ Добавлена колонка users.restriction_topup')
@@ -3510,7 +3503,7 @@ async def add_user_restriction_columns() -> bool:
text('ALTER TABLE users ADD COLUMN restriction_subscription BOOLEAN DEFAULT FALSE NOT NULL')
)
else:
- logger.error(f'Неподдерживаемый тип БД: {db_type}')
+ logger.error('Неподдерживаемый тип БД', db_type=db_type)
return False
logger.info('✅ Добавлена колонка users.restriction_subscription')
@@ -3518,14 +3511,14 @@ async def add_user_restriction_columns() -> bool:
if db_type == 'sqlite' or db_type == 'postgresql' or db_type == 'mysql':
await conn.execute(text('ALTER TABLE users ADD COLUMN restriction_reason VARCHAR(500) NULL'))
else:
- logger.error(f'Неподдерживаемый тип БД: {db_type}')
+ logger.error('Неподдерживаемый тип БД', db_type=db_type)
return False
logger.info('✅ Добавлена колонка users.restriction_reason')
return True
except Exception as e:
- logger.error(f'Ошибка добавления колонок ограничений пользователей: {e}')
+ logger.error('Ошибка добавления колонок ограничений пользователей', error=e)
return False
@@ -3565,17 +3558,17 @@ async def add_user_cabinet_columns() -> bool:
await conn.execute(text(f'ALTER TABLE users ADD COLUMN {col_name} {col_type}'))
added_count += 1
- logger.info(f'✅ Добавлена колонка users.{col_name}')
+ logger.info('✅ Добавлена колонка users.', col_name=col_name)
if added_count == 0:
logger.info('ℹ️ Все колонки cabinet уже существуют в таблице users')
else:
- logger.info(f'✅ Добавлено {added_count} колонок cabinet в таблицу users')
+ logger.info('✅ Добавлено колонок cabinet в таблицу users', added_count=added_count)
return True
except Exception as e:
- logger.error(f'Ошибка добавления колонок cabinet: {e}')
+ logger.error('Ошибка добавления колонок cabinet', error=e)
return False
@@ -3596,7 +3589,7 @@ async def add_subscription_crypto_link_column() -> bool:
elif db_type == 'mysql':
await conn.execute(text('ALTER TABLE subscriptions ADD COLUMN subscription_crypto_link VARCHAR(512)'))
else:
- logger.error(f'Неподдерживаемый тип БД для добавления subscription_crypto_link: {db_type}')
+ logger.error('Неподдерживаемый тип БД для добавления subscription_crypto_link', db_type=db_type)
return False
await conn.execute(
@@ -3609,7 +3602,7 @@ async def add_subscription_crypto_link_column() -> bool:
logger.info('✅ Добавлена колонка subscription_crypto_link в таблицу subscriptions')
return True
except Exception as e:
- logger.error(f'Ошибка добавления колонки subscription_crypto_link: {e}')
+ logger.error('Ошибка добавления колонки subscription_crypto_link', error=e)
return False
@@ -3630,13 +3623,13 @@ async def add_subscription_last_webhook_update_column() -> bool:
elif db_type == 'mysql':
await conn.execute(text('ALTER TABLE subscriptions ADD COLUMN last_webhook_update_at DATETIME'))
else:
- logger.error(f'Неподдерживаемый тип БД для добавления last_webhook_update_at: {db_type}')
+ logger.error('Неподдерживаемый тип БД для добавления last_webhook_update_at', db_type=db_type)
return False
logger.info('✅ Добавлена колонка last_webhook_update_at в таблицу subscriptions')
return True
except Exception as e:
- logger.error(f'Ошибка добавления колонки last_webhook_update_at: {e}')
+ logger.error('Ошибка добавления колонки last_webhook_update_at', error=e)
return False
@@ -3663,7 +3656,7 @@ async def fix_foreign_keys_for_user_deletion():
)
logger.info('Обновлен внешний ключ user_messages.created_by')
except Exception as e:
- logger.warning(f'Ошибка обновления FK user_messages: {e}')
+ logger.warning('Ошибка обновления FK user_messages', error=e)
try:
await conn.execute(
@@ -3682,13 +3675,13 @@ async def fix_foreign_keys_for_user_deletion():
)
logger.info('Обновлен внешний ключ promocodes.created_by')
except Exception as e:
- logger.warning(f'Ошибка обновления FK promocodes: {e}')
+ logger.warning('Ошибка обновления FK promocodes', error=e)
logger.info('Внешние ключи обновлены для безопасного удаления пользователей')
return True
except Exception as e:
- logger.error(f'Ошибка обновления внешних ключей: {e}')
+ logger.error('Ошибка обновления внешних ключей', error=e)
return False
@@ -3707,7 +3700,7 @@ async def add_referral_commission_percent_column() -> bool:
elif db_type == 'mysql':
alter_sql = 'ALTER TABLE users ADD COLUMN referral_commission_percent INT NULL'
else:
- logger.error(f'Неподдерживаемый тип БД для добавления referral_commission_percent: {db_type}')
+ logger.error('Неподдерживаемый тип БД для добавления referral_commission_percent', db_type=db_type)
return False
await conn.execute(text(alter_sql))
@@ -3715,7 +3708,7 @@ async def add_referral_commission_percent_column() -> bool:
return True
except Exception as error:
- logger.error(f'Ошибка добавления referral_commission_percent: {error}')
+ logger.error('Ошибка добавления referral_commission_percent', error=error)
return False
@@ -3757,7 +3750,7 @@ async def add_referral_system_columns():
result = await conn.execute(text(update_sql))
updated_count = result.rowcount
- logger.info(f'Обновлено {updated_count} пользователей с has_made_first_topup = TRUE')
+ logger.info('Обновлено пользователей с has_made_first_topup TRUE', updated_count=updated_count)
logger.info('✅ Миграция реферальной системы завершена')
return True
@@ -3765,7 +3758,7 @@ async def add_referral_system_columns():
return True
except Exception as e:
- logger.error(f'Ошибка миграции реферальной системы: {e}')
+ logger.error('Ошибка миграции реферальной системы', error=e)
return False
@@ -3833,7 +3826,7 @@ async def create_subscription_conversions_table():
CREATE INDEX idx_subscription_conversions_converted_at ON subscription_conversions(converted_at);
"""
else:
- logger.error(f'Неподдерживаемый тип БД для создания таблицы: {db_type}')
+ logger.error('Неподдерживаемый тип БД для создания таблицы', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -3841,7 +3834,7 @@ async def create_subscription_conversions_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы subscription_conversions: {e}')
+ logger.error('Ошибка создания таблицы subscription_conversions', error=e)
return False
@@ -3921,7 +3914,7 @@ async def create_subscription_events_table():
CREATE INDEX ix_subscription_events_user_id ON subscription_events(user_id);
"""
else:
- logger.error(f'Неподдерживаемый тип БД для создания таблицы subscription_events: {db_type}')
+ logger.error('Неподдерживаемый тип БД для создания таблицы subscription_events', db_type=db_type)
return False
await conn.execute(text(create_sql))
@@ -3929,14 +3922,14 @@ async def create_subscription_events_table():
return True
except Exception as e:
- logger.error(f'Ошибка создания таблицы subscription_events: {e}')
+ logger.error('Ошибка создания таблицы subscription_events', error=e)
return False
async def fix_subscription_duplicates_universal():
async with engine.begin() as conn:
db_type = await get_database_type()
- logger.info(f'Обнаружен тип базы данных: {db_type}')
+ logger.info('Обнаружен тип базы данных', db_type=db_type)
try:
result = await conn.execute(
@@ -3954,7 +3947,7 @@ async def fix_subscription_duplicates_universal():
logger.info('Дублирующихся подписок не найдено')
return 0
- logger.info(f'Найдено {len(duplicates)} пользователей с дублирующимися подписками')
+ logger.info('Найдено пользователей с дублирующимися подписками', duplicates_count=len(duplicates))
total_deleted = 0
@@ -4016,13 +4009,15 @@ async def fix_subscription_duplicates_universal():
deleted_count = delete_result.rowcount
total_deleted += deleted_count
- logger.info(f'Удалено {deleted_count} дублирующихся подписок для пользователя {user_id}')
+ logger.info(
+ 'Удалено дублирующихся подписок для пользователя', deleted_count=deleted_count, user_id=user_id
+ )
- logger.info(f'Всего удалено дублирующихся подписок: {total_deleted}')
+ logger.info('Всего удалено дублирующихся подписок', total_deleted=total_deleted)
return total_deleted
except Exception as e:
- logger.error(f'Ошибка при очистке дублирующихся подписок: {e}')
+ logger.error('Ошибка при очистке дублирующихся подписок', error=e)
raise
@@ -4113,14 +4108,14 @@ async def ensure_server_promo_groups_setup() -> bool:
assigned_count += 1
if assigned_count:
- logger.info(f'✅ Базовая промогруппа назначена {assigned_count} серверам')
+ logger.info('✅ Базовая промогруппа назначена серверам', assigned_count=assigned_count)
else:
logger.info('ℹ️ Все серверы уже имеют назначенные промогруппы')
return True
except Exception as e:
- logger.error(f'Ошибка настройки таблицы server_squad_promo_groups: {e}')
+ logger.error('Ошибка настройки таблицы server_squad_promo_groups', error=e)
return False
@@ -4150,7 +4145,7 @@ async def add_server_trial_flag_column() -> bool:
return True
except Exception as error:
- logger.error(f'Ошибка добавления колонки is_trial_eligible: {error}')
+ logger.error('Ошибка добавления колонки is_trial_eligible', error=error)
return False
@@ -4203,7 +4198,7 @@ async def create_system_settings_table() -> bool:
return True
except Exception as error:
- logger.error(f'Ошибка создания таблицы system_settings: {error}')
+ logger.error('Ошибка создания таблицы system_settings', error=error)
return False
@@ -4258,7 +4253,7 @@ async def create_menu_layout_history_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы menu_layout_history: {error}')
+ logger.error('❌ Ошибка создания таблицы menu_layout_history', error=error)
return False
@@ -4328,7 +4323,7 @@ async def create_button_click_logs_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы button_click_logs: {error}')
+ logger.error('❌ Ошибка создания таблицы button_click_logs', error=error)
return False
@@ -4396,7 +4391,7 @@ async def fix_button_click_logs_fk() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка исправления FK button_click_logs: {error}')
+ logger.error('❌ Ошибка исправления FK button_click_logs', error=error)
return False
@@ -4476,7 +4471,7 @@ async def create_web_api_tokens_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы web_api_tokens: {error}')
+ logger.error('❌ Ошибка создания таблицы web_api_tokens', error=error)
return False
@@ -4529,7 +4524,7 @@ async def create_privacy_policies_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы privacy_policies: {error}')
+ logger.error('❌ Ошибка создания таблицы privacy_policies', error=error)
return False
@@ -4582,7 +4577,7 @@ async def create_public_offers_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы public_offers: {error}')
+ logger.error('❌ Ошибка создания таблицы public_offers', error=error)
return False
@@ -4632,7 +4627,7 @@ async def create_faq_settings_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы faq_settings: {error}')
+ logger.error('❌ Ошибка создания таблицы faq_settings', error=error)
return False
@@ -4696,7 +4691,7 @@ async def create_faq_pages_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы faq_pages: {error}')
+ logger.error('❌ Ошибка создания таблицы faq_pages', error=error)
return False
@@ -4743,7 +4738,7 @@ async def ensure_default_web_api_token() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания дефолтного веб-API токена: {error}')
+ logger.error('❌ Ошибка создания дефолтного веб-API токена', error=error)
return False
@@ -4777,7 +4772,7 @@ async def add_promo_group_priority_column() -> bool:
return True
except Exception as error:
- logger.error(f'Ошибка добавления колонки priority: {error}')
+ logger.error('Ошибка добавления колонки priority', error=error)
return False
@@ -4838,7 +4833,7 @@ async def create_user_promo_groups_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы user_promo_groups: {error}')
+ logger.error('❌ Ошибка создания таблицы user_promo_groups', error=error)
return False
@@ -4861,7 +4856,7 @@ async def migrate_existing_user_promo_groups_data() -> bool:
count = result.scalar()
if count > 0:
- logger.info(f'ℹ️ В таблице user_promo_groups уже есть {count} записей, пропускаем миграцию')
+ logger.info('ℹ️ В таблице user_promo_groups уже есть записей, пропускаем миграцию', count=count)
return True
# Переносим данные из users.promo_group_id
@@ -4885,11 +4880,11 @@ async def migrate_existing_user_promo_groups_data() -> bool:
result = await conn.execute(text(migrate_sql))
migrated_count = result.rowcount if hasattr(result, 'rowcount') else 0
- logger.info(f'✅ Перенесено {migrated_count} связей пользователей с промогруппами')
+ logger.info('✅ Перенесено связей пользователей с промогруппами', migrated_count=migrated_count)
return True
except Exception as error:
- logger.error(f'❌ Ошибка миграции данных user_promo_groups: {error}')
+ logger.error('❌ Ошибка миграции данных user_promo_groups', error=error)
return False
@@ -4940,7 +4935,7 @@ async def add_promocode_promo_group_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления promo_group_id в promocodes: {error}')
+ logger.error('❌ Ошибка добавления promo_group_id в promocodes', error=error)
return False
@@ -4964,7 +4959,7 @@ async def add_promocode_first_purchase_only_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления first_purchase_only в promocodes: {error}')
+ logger.error('❌ Ошибка добавления first_purchase_only в promocodes', error=error)
return False
@@ -5018,7 +5013,7 @@ async def migrate_contest_templates_prize_columns() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка миграции prize_type/prize_value в contest_templates: {error}')
+ logger.error('❌ Ошибка миграции prize_type/prize_value в contest_templates', error=error)
return False
@@ -5044,7 +5039,7 @@ async def add_subscription_modem_enabled_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления modem_enabled в subscriptions: {error}')
+ logger.error('❌ Ошибка добавления modem_enabled в subscriptions', error=error)
return False
@@ -5068,7 +5063,7 @@ async def add_subscription_purchased_traffic_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления purchased_traffic_gb в subscriptions: {error}')
+ logger.error('❌ Ошибка добавления purchased_traffic_gb в subscriptions', error=error)
return False
@@ -5112,12 +5107,12 @@ async def add_transaction_receipt_columns() -> bool:
else:
await conn.execute(text('CREATE INDEX ix_transactions_receipt_uuid ON transactions (receipt_uuid)'))
except Exception as idx_error:
- logger.warning(f'Индекс на receipt_uuid возможно уже существует: {idx_error}')
+ logger.warning('Индекс на receipt_uuid возможно уже существует', idx_error=idx_error)
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонок чеков в transactions: {error}')
+ logger.error('❌ Ошибка добавления колонок чеков в transactions', error=error)
return False
@@ -5164,12 +5159,12 @@ async def add_oauth_provider_columns() -> bool:
else:
await conn.execute(text(f'CREATE UNIQUE INDEX uq_users_{col} ON users ({col})'))
except Exception as idx_error:
- logger.warning(f'Индекс uq_users_{col} возможно уже существует: {idx_error}')
+ logger.warning('Индекс uq_users_ возможно уже существует', col=col, idx_error=idx_error)
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонок OAuth провайдеров в users: {error}')
+ logger.error('❌ Ошибка добавления колонок OAuth провайдеров в users', error=error)
return False
@@ -5251,7 +5246,7 @@ async def create_withdrawal_requests_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы withdrawal_requests: {error}')
+ logger.error('❌ Ошибка создания таблицы withdrawal_requests', error=error)
return False
@@ -5314,7 +5309,7 @@ async def create_traffic_purchases_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы traffic_purchases: {error}')
+ logger.error('❌ Ошибка создания таблицы traffic_purchases', error=error)
return False
@@ -5398,7 +5393,7 @@ async def create_tariffs_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы tariffs: {error}')
+ logger.error('❌ Ошибка создания таблицы tariffs', error=error)
return False
@@ -5451,7 +5446,7 @@ async def create_tariff_promo_groups_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы tariff_promo_groups: {error}')
+ logger.error('❌ Ошибка создания таблицы tariff_promo_groups', error=error)
return False
@@ -5475,7 +5470,7 @@ async def ensure_tariff_max_device_limit_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонки max_device_limit: {error}')
+ logger.error('❌ Ошибка добавления колонки max_device_limit', error=error)
return False
@@ -5517,7 +5512,7 @@ async def add_subscription_tariff_id_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонки tariff_id: {error}')
+ logger.error('❌ Ошибка добавления колонки tariff_id', error=error)
return False
@@ -5606,7 +5601,7 @@ async def add_campaign_tariff_columns() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонок tariff в рекламные кампании: {error}')
+ logger.error('❌ Ошибка добавления колонок tariff в рекламные кампании', error=error)
return False
@@ -5629,7 +5624,7 @@ async def add_tariff_device_price_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонки device_price_kopeks: {error}')
+ logger.error('❌ Ошибка добавления колонки device_price_kopeks', error=error)
return False
@@ -5654,7 +5649,7 @@ async def add_tariff_server_traffic_limits_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонки server_traffic_limits: {error}')
+ logger.error('❌ Ошибка добавления колонки server_traffic_limits', error=error)
return False
@@ -5685,7 +5680,7 @@ async def add_tariff_allow_traffic_topup_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонки allow_traffic_topup: {error}')
+ logger.error('❌ Ошибка добавления колонки allow_traffic_topup', error=error)
return False
@@ -5916,7 +5911,7 @@ async def create_wheel_tables() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблиц для колеса удачи: {error}')
+ logger.error('❌ Ошибка создания таблиц для колеса удачи', error=error)
return False
@@ -5987,7 +5982,7 @@ async def add_tariff_traffic_topup_columns() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонок для докупки трафика: {error}')
+ logger.error('❌ Ошибка добавления колонок для докупки трафика', error=error)
return False
@@ -6033,7 +6028,7 @@ async def add_tariff_daily_columns() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонок суточного тарифа: {error}')
+ logger.error('❌ Ошибка добавления колонок суточного тарифа', error=error)
return False
@@ -6144,12 +6139,12 @@ async def add_tariff_custom_days_traffic_columns() -> bool:
logger.info('ℹ️ Колонка max_traffic_gb уже существует в tariffs')
if columns_added > 0:
- logger.info(f'✅ Добавлено {columns_added} колонок для произвольных дней/трафика')
+ logger.info('✅ Добавлено колонок для произвольных дней/трафика', columns_added=columns_added)
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонок произвольных дней/трафика: {error}')
+ logger.error('❌ Ошибка добавления колонок произвольных дней/трафика', error=error)
return False
@@ -6169,7 +6164,7 @@ async def add_tariff_traffic_reset_mode_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонки traffic_reset_mode: {error}')
+ logger.error('❌ Ошибка добавления колонки traffic_reset_mode', error=error)
return False
@@ -6221,7 +6216,7 @@ async def add_subscription_daily_columns() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонок суточной подписки: {error}')
+ logger.error('❌ Ошибка добавления колонок суточной подписки', error=error)
return False
@@ -6246,7 +6241,7 @@ async def add_subscription_traffic_reset_at_column() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка добавления колонки traffic_reset_at: {error}')
+ logger.error('❌ Ошибка добавления колонки traffic_reset_at', error=error)
return False
@@ -6271,7 +6266,7 @@ async def add_user_email_auth_columns() -> bool:
text("ALTER TABLE users ADD COLUMN auth_type VARCHAR(20) DEFAULT 'telegram' NOT NULL")
)
else:
- logger.error(f'Неподдерживаемый тип БД: {db_type}')
+ logger.error('Неподдерживаемый тип БД', db_type=db_type)
return False
logger.info('✅ Добавлена колонка users.auth_type')
else:
@@ -6325,7 +6320,7 @@ async def add_user_email_auth_columns() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка миграции email auth: {error}')
+ logger.error('❌ Ошибка миграции email auth', error=error)
return False
@@ -6389,7 +6384,7 @@ async def create_email_templates_table() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка создания таблицы email_templates: {error}')
+ logger.error('❌ Ошибка создания таблицы email_templates', error=error)
return False
@@ -6455,7 +6450,7 @@ async def migrate_cloudpayments_transaction_id_to_bigint() -> bool:
return True
except Exception as error:
- logger.error(f'❌ Ошибка миграции transaction_id_cp на BIGINT: {error}')
+ logger.error('❌ Ошибка миграции transaction_id_cp на BIGINT', error=error)
return False
@@ -6464,7 +6459,7 @@ async def run_universal_migration():
try:
db_type = await get_database_type()
- logger.info(f'Тип базы данных: {db_type}')
+ logger.info('Тип базы данных', db_type=db_type)
if db_type == 'postgresql':
logger.info('=== СИНХРОНИЗАЦИЯ ПОСЛЕДОВАТЕЛЬНОСТЕЙ PostgreSQL ===')
@@ -6576,18 +6571,18 @@ async def run_universal_migration():
logger.warning('⚠️ Проблемы с таблицей Heleket payments')
mulenpay_name = settings.get_mulenpay_display_name()
- logger.info('=== СОЗДАНИЕ ТАБЛИЦЫ %s ===', mulenpay_name)
+ logger.info('=== СОЗДАНИЕ ТАБЛИЦЫ =', mulenpay_name=mulenpay_name)
mulenpay_created = await create_mulenpay_payments_table()
if mulenpay_created:
- logger.info('✅ Таблица %s payments готова', mulenpay_name)
+ logger.info('✅ Таблица payments готова', mulenpay_name=mulenpay_name)
else:
- logger.warning('⚠️ Проблемы с таблицей %s payments', mulenpay_name)
+ logger.warning('⚠️ Проблемы с таблицей payments', mulenpay_name=mulenpay_name)
mulenpay_schema_ok = await ensure_mulenpay_payment_schema()
if mulenpay_schema_ok:
- logger.info('✅ Схема %s payments актуальна', mulenpay_name)
+ logger.info('✅ Схема payments актуальна', mulenpay_name=mulenpay_name)
else:
- logger.warning('⚠️ Не удалось обновить схему %s payments', mulenpay_name)
+ logger.warning('⚠️ Не удалось обновить схему payments', mulenpay_name=mulenpay_name)
logger.info('=== СОЗДАНИЕ ТАБЛИЦЫ PAL24 ===')
pal24_created = await create_pal24_payments_table()
@@ -6821,14 +6816,12 @@ async def run_universal_migration():
else:
logger.warning('⚠️ Не удалось обновить колонку last_pinned_message_id')
- logger.info('=== ДОБАВЛЕНИЕ МЕДИА ПОЛЕЙ В BROADCAST_HISTORY ===')
media_fields_added = await add_media_fields_to_broadcast_history()
if media_fields_added:
logger.info('✅ Медиа поля в broadcast_history готовы')
else:
logger.warning('⚠️ Проблемы с добавлением медиа полей')
- logger.info('=== ДОБАВЛЕНИЕ EMAIL ПОЛЕЙ В BROADCAST_HISTORY ===')
email_fields_added = await add_email_fields_to_broadcast_history()
if email_fields_added:
logger.info('✅ Email поля в broadcast_history готовы')
@@ -6948,9 +6941,8 @@ async def run_universal_migration():
else:
logger.info('ℹ️ Таблица support_audit_logs уже существует')
except Exception as e:
- logger.warning(f'⚠️ Проблемы с созданием таблицы support_audit_logs: {e}')
+ logger.warning('⚠️ Проблемы с созданием таблицы support_audit_logs', error=e)
- logger.info('=== НАСТРОЙКА ПРОМО ГРУПП ===')
promo_groups_ready = await ensure_promo_groups_setup()
if promo_groups_ready:
logger.info('✅ Промо группы готовы')
@@ -7145,8 +7137,8 @@ async def run_universal_migration():
total_count = total_subs.fetchone()[0]
unique_count = unique_users.fetchone()[0]
- logger.info(f'Всего подписок: {total_count}')
- logger.info(f'Уникальных пользователей: {unique_count}')
+ logger.info('Всего подписок', total_count=total_count)
+ logger.info('Уникальных пользователей', unique_count=unique_count)
if total_count == unique_count:
logger.info('База данных уже в корректном состоянии')
@@ -7168,7 +7160,9 @@ async def run_universal_migration():
remaining_duplicates = final_check.fetchall()
if remaining_duplicates:
- logger.warning(f'Остались дубликаты у {len(remaining_duplicates)} пользователей')
+ logger.warning(
+ 'Остались дубликаты у пользователей', remaining_duplicates_count=len(remaining_duplicates)
+ )
return False
logger.info('=== МИГРАЦИЯ ЗАВЕРШЕНА УСПЕШНО ===')
logger.info('✅ Реферальная система обновлена')
@@ -7182,7 +7176,7 @@ async def run_universal_migration():
return True
except Exception as e:
- logger.error(f'=== ОШИБКА ВЫПОЛНЕНИЯ МИГРАЦИИ: {e} ===')
+ logger.error('ОШИБКА ВЫПОЛНЕНИЯ МИГРАЦИИ', error=e)
return False
@@ -7470,7 +7464,9 @@ async def check_migration_status():
for check_key, check_status in status.items():
check_name = check_names.get(check_key, check_key)
icon = '✅' if check_status else '❌'
- logger.info(f'{icon} {check_name}: {"OK" if check_status else "ТРЕБУЕТ ВНИМАНИЯ"}')
+ logger.info(
+ 'log event', icon=icon, check_name=check_name, value='OK' if check_status else 'ТРЕБУЕТ ВНИМАНИЯ'
+ )
all_good = all(status.values())
if all_good:
@@ -7489,18 +7485,22 @@ async def check_migration_status():
broadcast_count = broadcasts_count.fetchone()[0]
logger.info(
- f'📊 Статистика: {usr_count} пользователей, {conv_count} конверсий, {welcome_count} приветственных текстов, {broadcast_count} рассылок'
+ '📊 Статистика: пользователей, конверсий, приветственных текстов, рассылок',
+ usr_count=usr_count,
+ conv_count=conv_count,
+ welcome_count=welcome_count,
+ broadcast_count=broadcast_count,
)
except Exception as stats_error:
- logger.debug(f'Не удалось получить дополнительную статистику: {stats_error}')
+ logger.debug('Не удалось получить дополнительную статистику', stats_error=stats_error)
else:
logger.warning('⚠️ Некоторые миграции требуют внимания')
missing_migrations = [check_names[k] for k, v in status.items() if not v]
- logger.warning(f'Требуют выполнения: {", ".join(missing_migrations)}')
+ logger.warning('Требуют выполнения', value=', '.join(missing_migrations))
return status
except Exception as e:
- logger.error(f'Ошибка проверки статуса миграций: {e}')
+ logger.error('Ошибка проверки статуса миграций', error=e)
return None
diff --git a/app/external/ban_system_api.py b/app/external/ban_system_api.py
index 7ad39479..a0903406 100644
--- a/app/external/ban_system_api.py
+++ b/app/external/ban_system_api.py
@@ -4,13 +4,13 @@ Ban System API Client.
Client for interacting with the BedolagaBan monitoring system.
"""
-import logging
from typing import Any
import aiohttp
+import structlog
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class BanSystemAPIError(Exception):
@@ -78,7 +78,7 @@ class BanSystemAPI:
response_text = await response.text()
if response.status >= 400:
- logger.error(f'Ban System API error: {response.status} - {response_text}')
+ logger.error('Ban System API error', status=response.status, response_text=response_text)
raise BanSystemAPIError(
message=f'API error {response.status}: {response_text}',
status_code=response.status,
@@ -93,7 +93,7 @@ class BanSystemAPI:
return {}
except aiohttp.ClientError as e:
- logger.error(f'Ban System API connection error: {e}')
+ logger.error('Ban System API connection error', error=e)
raise BanSystemAPIError(message=f'Connection error: {e!s}', status_code=None, response_data=None)
except TimeoutError:
logger.error('Ban System API request timeout')
diff --git a/app/external/cryptobot.py b/app/external/cryptobot.py
index d91d4849..75cf9165 100644
--- a/app/external/cryptobot.py
+++ b/app/external/cryptobot.py
@@ -1,14 +1,14 @@
import hashlib
import hmac
-import logging
from typing import Any
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class CryptoBotService:
@@ -49,11 +49,11 @@ class CryptoBotService:
if response.status == 200 and response_data.get('ok'):
return response_data.get('result')
- logger.error(f'CryptoBot API ошибка: {response_data}')
+ logger.error('CryptoBot API ошибка', response_data=response_data)
return None
except Exception as e:
- logger.error(f'Ошибка запроса к CryptoBot API: {e}')
+ logger.error('Ошибка запроса к CryptoBot API', error=e)
return None
async def get_me(self) -> dict[str, Any] | None:
@@ -81,7 +81,7 @@ class CryptoBotService:
result = await self._make_request('POST', 'createInvoice', data)
if result:
- logger.info(f'Создан CryptoBot invoice {result.get("invoice_id")} на {amount} {asset}')
+ logger.info('Создан CryptoBot invoice на', get=result.get('invoice_id'), amount=amount, asset=asset)
return result
@@ -140,7 +140,7 @@ class CryptoBotService:
return is_valid
except Exception as e:
- logger.error(f'Ошибка проверки подписи CryptoBot webhook: {e}')
+ logger.error('Ошибка проверки подписи CryptoBot webhook', error=e)
return False
async def process_webhook(self, webhook_data: dict[str, Any]) -> dict[str, Any] | None:
@@ -161,9 +161,9 @@ class CryptoBotService:
'payment_system': 'cryptobot',
}
- logger.warning(f'Неизвестный тип CryptoBot webhook: {update_type}')
+ logger.warning('Неизвестный тип CryptoBot webhook', update_type=update_type)
return None
except Exception as e:
- logger.error(f'Ошибка обработки CryptoBot webhook: {e}')
+ logger.error('Ошибка обработки CryptoBot webhook', error=e)
return None
diff --git a/app/external/heleket.py b/app/external/heleket.py
index b4ed0163..5694c94f 100644
--- a/app/external/heleket.py
+++ b/app/external/heleket.py
@@ -6,15 +6,15 @@ import base64
import hashlib
import hmac
import json
-import logging
from typing import Any
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class HeleketService:
@@ -93,26 +93,28 @@ class HeleketService:
):
text = await response.text()
if response.content_type != 'application/json':
- logger.error('Ответ Heleket не JSON (%s): %s', response.content_type, text)
+ logger.error('Ответ Heleket не JSON', content_type=response.content_type, text=text)
return None
try:
data = json.loads(text)
except json.JSONDecodeError:
- logger.error('Ошибка парсинга Heleket JSON: %s', text)
+ logger.error('Ошибка парсинга Heleket JSON', text=text)
return None
if response.status >= 400:
- logger.error('Heleket API %s вернул статус %s: %s', endpoint, response.status, data)
+ logger.error(
+ 'Heleket API вернул статус', endpoint=endpoint, response_status=response.status, data=data
+ )
return None
if isinstance(data, dict) and data.get('state') == 0:
return data
- logger.error('Heleket API вернул ошибку: %s', data)
+ logger.error('Heleket API вернул ошибку', data=data)
return None
except Exception as error: # pragma: no cover - defensive
- logger.error('Ошибка запроса к Heleket API: %s', error)
+ logger.error('Ошибка запроса к Heleket API', error=error)
return None
async def create_payment(self, payload: dict[str, Any]) -> dict[str, Any] | None:
@@ -157,7 +159,7 @@ class HeleketService:
return True
if not isinstance(payload, dict):
- logger.error('Heleket webhook payload не dict: %s', payload)
+ logger.error('Heleket webhook payload не dict', payload=payload)
return False
signature = payload.get('sign')
@@ -173,5 +175,7 @@ class HeleketService:
is_valid = hmac.compare_digest(expected, str(signature))
if not is_valid:
- logger.error('Неверная подпись Heleket webhook: ожидается %s, получено %s', expected, signature)
+ logger.error(
+ 'Неверная подпись Heleket webhook: ожидается , получено', expected=expected, signature=signature
+ )
return is_valid
diff --git a/app/external/heleket_webhook.py b/app/external/heleket_webhook.py
index 7bea2ad6..05f267e7 100644
--- a/app/external/heleket_webhook.py
+++ b/app/external/heleket_webhook.py
@@ -1,8 +1,8 @@
import asyncio
import json
-import logging
from typing import Any
+import structlog
from aiohttp import web
from app.config import settings
@@ -11,7 +11,7 @@ from app.external.heleket import HeleketService
from app.services.payment_service import PaymentService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class HeleketWebhookHandler:
@@ -39,7 +39,7 @@ class HeleketWebhookHandler:
processed = await self.payment_service.process_heleket_webhook(db, payload)
await db.commit()
except Exception as e:
- logger.error(f'Ошибка обработки Heleket webhook: {e}')
+ logger.error('Ошибка обработки Heleket webhook', error=e)
await db.rollback()
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
@@ -97,15 +97,15 @@ async def start_heleket_webhook_server(payment_service: PaymentService) -> None:
try:
await site.start()
logger.info(
- 'Heleket webhook сервер запущен на %s:%s',
- settings.HELEKET_WEBHOOK_HOST,
- settings.HELEKET_WEBHOOK_PORT,
+ 'Heleket webhook сервер запущен на',
+ HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST,
+ HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT,
)
logger.info(
- 'Heleket webhook URL: http://%s:%s%s',
- settings.HELEKET_WEBHOOK_HOST,
- settings.HELEKET_WEBHOOK_PORT,
- settings.HELEKET_WEBHOOK_PATH,
+ 'Heleket webhook URL: http://',
+ HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST,
+ HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT,
+ HELEKET_WEBHOOK_PATH=settings.HELEKET_WEBHOOK_PATH,
)
while True:
diff --git a/app/external/pal24_client.py b/app/external/pal24_client.py
index 999e4404..e2abe069 100644
--- a/app/external/pal24_client.py
+++ b/app/external/pal24_client.py
@@ -3,17 +3,17 @@
from __future__ import annotations
import hashlib
-import logging
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from typing import Any
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class Pal24APIError(Exception):
@@ -96,30 +96,21 @@ class Pal24Client:
payload = await response.json(content_type=None)
except aiohttp.ContentTypeError:
text_body = await response.text()
- logger.error(
- 'Pal24 API returned non-JSON response for %s: %s',
- endpoint,
- text_body,
- )
+ logger.error('Pal24 API returned non-JSON response for', endpoint=endpoint, text_body=text_body)
raise Pal24APIError(f'Pal24 API returned non-JSON response: {text_body}') from None
result = Pal24Response.from_payload(payload, status)
if status >= 400 or not result.success:
- logger.error(
- 'Pal24 API error %s %s: %s',
- status,
- endpoint,
- payload,
- )
+ logger.error('Pal24 API error', status=status, endpoint=endpoint, payload=payload)
result.raise_for_status(endpoint)
return result
except TimeoutError as error:
- logger.error('Pal24 API request timeout for %s: %s', endpoint, error)
+ logger.error('Pal24 API request timeout for', endpoint=endpoint, error=error)
raise Pal24APIError(f'Pal24 API request timeout for {endpoint}') from error
except aiohttp.ClientError as error:
- logger.error('Pal24 API client error for %s: %s', endpoint, error)
+ logger.error('Pal24 API client error for', endpoint=endpoint, error=error)
raise Pal24APIError(str(error)) from error
# API methods -----------------------------------------------------------------
diff --git a/app/external/remnawave_api.py b/app/external/remnawave_api.py
index 087beb04..ef571e4a 100644
--- a/app/external/remnawave_api.py
+++ b/app/external/remnawave_api.py
@@ -1,7 +1,6 @@
import asyncio
import base64
import json
-import logging
import ssl
from dataclasses import dataclass
from datetime import datetime
@@ -10,9 +9,10 @@ from typing import Any
from urllib.parse import urlparse
import aiohttp
+import structlog
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class UserStatus(Enum):
@@ -310,7 +310,7 @@ class RemnaWaveAPI:
async def __aenter__(self):
conn_type = self._detect_connection_type()
- logger.debug(f'Подключение к Remnawave: {self.base_url} (тип: {conn_type})')
+ logger.debug('Подключение к Remnawave: (тип: )', base_url=self.base_url, conn_type=conn_type)
headers = self._prepare_auth_headers()
@@ -319,10 +319,10 @@ class RemnaWaveAPI:
if ':' in self.secret_key:
key_name, key_value = self.secret_key.split(':', 1)
cookies = {key_name: key_value}
- logger.debug(f'Используем куки: {key_name}=***')
+ logger.debug('Используем куки: =***', key_name=key_name)
else:
cookies = {self.secret_key: self.secret_key}
- logger.debug(f'Используем куки: {self.secret_key}=***')
+ logger.debug('Используем куки: =***', secret_key=self.secret_key)
connector_kwargs = {}
@@ -388,12 +388,12 @@ class RemnaWaveAPI:
if response.status == 429 and attempt < max_retries:
retry_after = float(response.headers.get('Retry-After', base_delay * (2**attempt)))
logger.warning(
- 'Rate limited (429) on %s %s, retry %d/%d after %.1fs',
- method,
- endpoint,
- attempt + 1,
- max_retries,
- retry_after,
+ 'Rate limited (429) on , retry / after s',
+ method=method,
+ endpoint=endpoint,
+ attempt=attempt + 1,
+ max_retries=max_retries,
+ retry_after=retry_after,
)
await asyncio.sleep(retry_after)
continue
@@ -411,17 +411,17 @@ class RemnaWaveAPI:
if attempt < max_retries:
delay = base_delay * (2**attempt)
logger.warning(
- 'Request failed on %s %s: %s, retry %d/%d after %.1fs',
- method,
- endpoint,
- e,
- attempt + 1,
- max_retries,
- delay,
+ 'Request failed on retry / after s',
+ method=method,
+ endpoint=endpoint,
+ e=e,
+ attempt=attempt + 1,
+ max_retries=max_retries,
+ delay=delay,
)
await asyncio.sleep(delay)
continue
- logger.error(f'Request failed: {e}')
+ logger.error('Request failed', error=e)
raise RemnaWaveAPIError(f'Request failed: {e!s}')
raise RemnaWaveAPIError(f'Max retries exceeded for {method} {endpoint}')
@@ -461,7 +461,7 @@ class RemnaWaveAPI:
if active_internal_squads:
data['activeInternalSquads'] = active_internal_squads
- logger.debug('Создание пользователя в панели: %s', data)
+ logger.debug('Создание пользователя в панели', data=data)
response = await self._make_request('POST', '/api/users', data)
user = self._parse_user(response['response'])
return await self.enrich_user_with_happ_link(user)
@@ -1049,13 +1049,13 @@ class RemnaWaveAPI:
delete_data = {'userUuid': user_uuid, 'hwid': device_hwid}
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
except Exception as device_error:
- logger.error(f'Ошибка удаления устройства {device_hwid}: {device_error}')
+ logger.error('Ошибка удаления устройства', device_hwid=device_hwid, device_error=device_error)
failed_count += 1
return failed_count < len(devices) / 2
except Exception as e:
- logger.error(f'Ошибка при сбросе устройств: {e}')
+ logger.error('Ошибка при сбросе устройств', error=e)
return False
async def remove_device(self, user_uuid: str, device_hwid: str) -> bool:
@@ -1064,7 +1064,7 @@ class RemnaWaveAPI:
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
return True
except Exception as e:
- logger.error(f'Ошибка удаления устройства {device_hwid}: {e}')
+ logger.error('Ошибка удаления устройства', device_hwid=device_hwid, error=e)
return False
async def encrypt_happ_crypto_link(self, link_to_encrypt: str) -> str | None:
@@ -1073,10 +1073,10 @@ class RemnaWaveAPI:
response = await self._make_request('POST', '/api/system/tools/happ/encrypt', data)
return response.get('response', {}).get('encryptedLink')
except RemnaWaveAPIError as e:
- logger.warning(f'Не удалось зашифровать happ ссылку: {e.message}')
+ logger.warning('Не удалось зашифровать happ ссылку', message=e.message)
return None
except Exception as e:
- logger.warning(f'Ошибка при шифровании happ ссылки: {e}')
+ logger.warning('Ошибка при шифровании happ ссылки', error=e)
return None
async def enrich_user_with_happ_link(self, user: RemnaWaveUser) -> RemnaWaveUser:
@@ -1112,7 +1112,7 @@ class RemnaWaveAPI:
try:
status = UserStatus(status_str)
except ValueError:
- logger.warning(f'Неизвестный статус пользователя: {status_str}, используем ACTIVE')
+ logger.warning('Неизвестный статус пользователя: используем ACTIVE', status_str=status_str)
status = UserStatus.ACTIVE
# Получаем trafficLimitStrategy с fallback
@@ -1120,7 +1120,7 @@ class RemnaWaveAPI:
try:
traffic_strategy = TrafficLimitStrategy(strategy_str)
except ValueError:
- logger.warning(f'Неизвестная стратегия трафика: {strategy_str}, используем NO_RESET')
+ logger.warning('Неизвестная стратегия трафика: используем NO_RESET', strategy_str=strategy_str)
traffic_strategy = TrafficLimitStrategy.NO_RESET
return RemnaWaveUser(
@@ -1285,5 +1285,5 @@ async def test_api_connection(api: RemnaWaveAPI) -> bool:
await api.get_system_stats()
return True
except Exception as e:
- logger.warning('API connection test failed: %s', e)
+ logger.warning('API connection test failed', e=e)
return False
diff --git a/app/external/telegram_stars.py b/app/external/telegram_stars.py
index bb1f3e49..0b4560c7 100644
--- a/app/external/telegram_stars.py
+++ b/app/external/telegram_stars.py
@@ -1,14 +1,14 @@
-import logging
from decimal import ROUND_HALF_UP, Decimal
from typing import Any
+import structlog
from aiogram import Bot
from aiogram.types import InlineKeyboardMarkup, LabeledPrice
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class TelegramStarsService:
@@ -49,13 +49,16 @@ class TelegramStarsService:
)
logger.info(
- f'Создан Stars invoice на {stars_amount} звезд (~{settings.format_price(amount_kopeks)}) '
- f'для {chat_id}, курс: {stars_rate}₽/⭐'
+ 'Создан Stars invoice на звезд (~) для курс: ₽/⭐',
+ stars_amount=stars_amount,
+ settings=settings.format_price(amount_kopeks),
+ chat_id=chat_id,
+ stars_rate=stars_rate,
)
return invoice_link
except Exception as e:
- logger.error(f'Ошибка создания Stars invoice: {e}')
+ logger.error('Ошибка создания Stars invoice', error=e)
return None
async def send_invoice(
@@ -84,8 +87,11 @@ class TelegramStarsService:
)
logger.info(
- f'Отправлен Stars invoice {message.message_id} на {stars_amount} звезд '
- f'(~{settings.format_price(amount_kopeks)}), курс: {stars_rate}₽/⭐'
+ 'Отправлен Stars invoice на звезд (~), курс: ₽/⭐',
+ message_id=message.message_id,
+ stars_amount=stars_amount,
+ settings=settings.format_price(amount_kopeks),
+ stars_rate=stars_rate,
)
return {
'message_id': message.message_id,
@@ -95,7 +101,7 @@ class TelegramStarsService:
}
except Exception as e:
- logger.error(f'Ошибка отправки Stars invoice: {e}')
+ logger.error('Ошибка отправки Stars invoice', error=e)
return None
async def answer_pre_checkout_query(
@@ -105,8 +111,8 @@ class TelegramStarsService:
await self.bot.answer_pre_checkout_query(
pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message
)
- logger.info(f'Ответ на pre_checkout_query: ok={ok}')
+ logger.info('Ответ на pre_checkout_query: ok', ok=ok)
return True
except Exception as e:
- logger.error(f'Ошибка ответа на pre_checkout_query: {e}')
+ logger.error('Ошибка ответа на pre_checkout_query', error=e)
return False
diff --git a/app/external/tribute.py b/app/external/tribute.py
index 7fc1b948..af40608a 100644
--- a/app/external/tribute.py
+++ b/app/external/tribute.py
@@ -1,13 +1,14 @@
import hashlib
import hmac
import json
-import logging
from typing import Any
+import structlog
+
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class TributeService:
@@ -25,11 +26,11 @@ class TributeService:
try:
payment_url = f'{self.donate_link}&user_id={user_id}'
- logger.info(f'Создана ссылка Tribute для пользователя {user_id}')
+ logger.info('Создана ссылка Tribute для пользователя', user_id=user_id)
return payment_url
except Exception as e:
- logger.error(f'Ошибка создания Tribute ссылки: {e}')
+ logger.error('Ошибка создания Tribute ссылки', error=e)
return None
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
@@ -50,7 +51,7 @@ class TributeService:
return is_valid
except Exception as e:
- logger.error(f'Ошибка проверки подписи webhook: {e}')
+ logger.error('Ошибка проверки подписи webhook', error=e)
return False
async def process_webhook(self, payload_or_data) -> dict[str, Any] | None:
@@ -60,9 +61,9 @@ class TributeService:
if isinstance(payload_or_data, str):
try:
webhook_data = json.loads(payload_or_data)
- logger.info(f'📊 Распарсенные данные: {webhook_data}')
+ logger.info('📊 Распарсенные данные', webhook_data=webhook_data)
except json.JSONDecodeError as e:
- logger.error(f'❌ Ошибка парсинга JSON: {e}')
+ logger.error('❌ Ошибка парсинга JSON', error=e)
return None
else:
webhook_data = payload_or_data
@@ -99,18 +100,24 @@ class TributeService:
status = 'unknown'
logger.info(
- f'📝 Извлеченные данные: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}'
+ '📝 Извлеченные данные: payment_id=, status=, amount_kopeks=, user_id',
+ payment_id=payment_id,
+ status=status,
+ amount_kopeks=amount_kopeks,
+ telegram_user_id=telegram_user_id,
)
if not telegram_user_id:
logger.error('❌ Не найден telegram_user_id в webhook данных')
- logger.error(f'🔍 Полные данные для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}')
+ logger.error(
+ '🔍 Полные данные для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2)
+ )
return None
try:
telegram_user_id = int(telegram_user_id)
except (ValueError, TypeError):
- logger.error(f'❌ Некорректный telegram_user_id: {telegram_user_id}')
+ logger.error('❌ Некорректный telegram_user_id', telegram_user_id=telegram_user_id)
return None
result = {
@@ -123,28 +130,28 @@ class TributeService:
'payment_system': 'tribute',
}
- logger.info(f'✅ Tribute webhook обработан успешно: {result}')
+ logger.info('✅ Tribute webhook обработан успешно', result=result)
return result
except Exception as e:
- logger.error(f'❌ Ошибка обработки Tribute webhook: {e}', exc_info=True)
- logger.error(f'🔍 Webhook data для отладки: {json.dumps(webhook_data, ensure_ascii=False, indent=2)}')
+ logger.error('❌ Ошибка обработки Tribute webhook', error=e, exc_info=True)
+ logger.error('🔍 Webhook data для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2))
return None
async def get_payment_status(self, payment_id: str) -> dict[str, Any] | None:
try:
- logger.info(f'Запрос статуса платежа {payment_id}')
+ logger.info('Запрос статуса платежа', payment_id=payment_id)
return {'status': 'unknown', 'payment_id': payment_id}
except Exception as e:
- logger.error(f'Ошибка получения статуса платежа: {e}')
+ logger.error('Ошибка получения статуса платежа', error=e)
return None
async def refund_payment(
self, payment_id: str, amount_kopeks: int | None = None, reason: str = 'Возврат по запросу'
) -> dict[str, Any] | None:
try:
- logger.info(f'Создание возврата для платежа {payment_id}')
+ logger.info('Создание возврата для платежа', payment_id=payment_id)
return {'refund_id': f'refund_{payment_id}', 'status': 'pending'}
except Exception as e:
- logger.error(f'Ошибка создания возврата: {e}')
+ logger.error('Ошибка создания возврата', error=e)
return None
diff --git a/app/external/wata_webhook.py b/app/external/wata_webhook.py
index eea4f22e..dbcfbfe5 100644
--- a/app/external/wata_webhook.py
+++ b/app/external/wata_webhook.py
@@ -3,11 +3,11 @@ from __future__ import annotations
import asyncio
import base64
import json
-import logging
from datetime import datetime, timedelta
from typing import Any
import aiohttp
+import structlog
from aiohttp import web
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
@@ -18,7 +18,7 @@ from app.database.database import AsyncSessionLocal
from app.services.payment_service import PaymentService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class WataPublicKeyProvider:
@@ -67,28 +67,24 @@ class WataPublicKeyProvider:
async with aiohttp.ClientSession(timeout=timeout) as session, session.get(url) as response:
text = await response.text()
if response.status >= 400:
- logger.error(
- 'Ошибка получения публичного ключа WATA %s: %s',
- response.status,
- text,
- )
+ logger.error('Ошибка получения публичного ключа WATA', response_status=response.status, text=text)
return None
try:
payload = await response.json()
except aiohttp.ContentTypeError:
- logger.error('Ответ WATA public-key не является JSON: %s', text)
+ logger.error('Ответ WATA public-key не является JSON', text=text)
return None
if isinstance(payload, dict):
value = payload.get('value')
if value:
return value
- logger.error('Ответ WATA public-key не содержит ключ: %s', payload)
+ logger.error('Ответ WATA public-key не содержит ключ', payload=payload)
else:
- logger.error('Неожиданный формат ответа WATA public-key: %s', payload)
+ logger.error('Неожиданный формат ответа WATA public-key', payload=payload)
except Exception as error:
- logger.error('Ошибка запроса публичного ключа WATA: %s', error)
+ logger.error('Ошибка запроса публичного ключа WATA', error=error)
return None
@@ -125,7 +121,7 @@ class WataWebhookHandler:
try:
public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
except ValueError as error:
- logger.error('Ошибка загрузки публичного ключа WATA: %s', error)
+ logger.error('Ошибка загрузки публичного ключа WATA', error=error)
return False
try:
@@ -140,7 +136,7 @@ class WataWebhookHandler:
logger.warning('Подпись WATA webhook не прошла проверку')
return False
except Exception as error:
- logger.error('Ошибка проверки подписи WATA: %s', error)
+ logger.error('Ошибка проверки подписи WATA', error=error)
return False
async def handle_webhook(self, request: web.Request) -> web.Response:
@@ -164,9 +160,9 @@ class WataWebhookHandler:
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
logger.info(
- 'Получен WATA webhook: order_id=%s, status=%s',
- payload.get('orderId'),
- payload.get('transactionStatus'),
+ 'Получен WATA webhook: order_id status',
+ payload=payload.get('orderId'),
+ payload_2=payload.get('transactionStatus'),
)
processed: bool | None = None
@@ -175,7 +171,7 @@ class WataWebhookHandler:
processed = await self.payment_service.process_wata_webhook(db, payload)
await db.commit()
except Exception as e:
- logger.error(f'Ошибка обработки WATA webhook: {e}')
+ logger.error('Ошибка обработки WATA webhook', error=e)
await db.rollback()
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
@@ -218,10 +214,7 @@ def create_wata_webhook_app(payment_service: PaymentService) -> web.Application:
app.router.add_options(settings.WATA_WEBHOOK_PATH, handler.options_handler)
app.router.add_get('/health', handler.health_check)
- logger.info(
- 'Настроен WATA webhook endpoint на %s',
- settings.WATA_WEBHOOK_PATH,
- )
+ logger.info('Настроен WATA webhook endpoint на', WATA_WEBHOOK_PATH=settings.WATA_WEBHOOK_PATH)
return app
@@ -244,15 +237,15 @@ async def start_wata_webhook_server(payment_service: PaymentService) -> None:
try:
await site.start()
logger.info(
- 'WATA webhook сервер запущен на %s:%s',
- settings.WATA_WEBHOOK_HOST,
- settings.WATA_WEBHOOK_PORT,
+ 'WATA webhook сервер запущен на',
+ WATA_WEBHOOK_HOST=settings.WATA_WEBHOOK_HOST,
+ WATA_WEBHOOK_PORT=settings.WATA_WEBHOOK_PORT,
)
logger.info(
- 'WATA webhook URL: http://%s:%s%s',
- settings.WATA_WEBHOOK_HOST,
- settings.WATA_WEBHOOK_PORT,
- settings.WATA_WEBHOOK_PATH,
+ 'WATA webhook URL: http://',
+ WATA_WEBHOOK_HOST=settings.WATA_WEBHOOK_HOST,
+ WATA_WEBHOOK_PORT=settings.WATA_WEBHOOK_PORT,
+ WATA_WEBHOOK_PATH=settings.WATA_WEBHOOK_PATH,
)
while True:
diff --git a/app/external/webhook_server.py b/app/external/webhook_server.py
index 483327f5..346eaef8 100644
--- a/app/external/webhook_server.py
+++ b/app/external/webhook_server.py
@@ -2,9 +2,9 @@ import base64
import hashlib
import hmac
import json
-import logging
from collections.abc import Iterable
+import structlog
from aiogram import Bot
from aiohttp import web
@@ -14,7 +14,7 @@ from app.services.payment_service import PaymentService
from app.services.tribute_service import TributeService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class WebhookServer:
@@ -51,8 +51,8 @@ class WebhookServer:
missing.append('FREEKASSA_SECRET_WORD_2')
if missing:
logger.warning(
- f'Freekassa ENABLED=true, но webhook не зарегистрирован. '
- f'Отсутствуют параметры: {", ".join(missing)}'
+ 'Freekassa ENABLED=true, но webhook не зарегистрирован. Отсутствуют параметры',
+ value=', '.join(missing),
)
self.app.router.add_get('/health', self._health_check)
@@ -66,18 +66,16 @@ class WebhookServer:
self.app.router.add_options(settings.FREEKASSA_WEBHOOK_PATH, self._options_handler)
logger.info('Webhook сервер настроен:')
- logger.info(f' - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}')
+ logger.info('Tribute webhook: POST', TRIBUTE_WEBHOOK_PATH=settings.TRIBUTE_WEBHOOK_PATH)
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
- ' - %s webhook: POST %s',
- mulenpay_name,
- settings.MULENPAY_WEBHOOK_PATH,
+ '- webhook: POST', mulenpay_name=mulenpay_name, MULENPAY_WEBHOOK_PATH=settings.MULENPAY_WEBHOOK_PATH
)
if settings.is_cryptobot_enabled():
- logger.info(f' - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}')
+ logger.info('CryptoBot webhook: POST', CRYPTOBOT_WEBHOOK_PATH=settings.CRYPTOBOT_WEBHOOK_PATH)
if settings.is_freekassa_enabled():
- logger.info(f' - Freekassa webhook: POST {settings.FREEKASSA_WEBHOOK_PATH}')
+ logger.info('Freekassa webhook: POST', FREEKASSA_WEBHOOK_PATH=settings.FREEKASSA_WEBHOOK_PATH)
logger.info(' - Health check: GET /health')
return self.app
@@ -95,35 +93,35 @@ class WebhookServer:
await self.site.start()
logger.info(
- 'Webhook сервер запущен на %s:%s',
- settings.TRIBUTE_WEBHOOK_HOST,
- settings.TRIBUTE_WEBHOOK_PORT,
+ 'Webhook сервер запущен на',
+ TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
+ TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
)
logger.info(
- 'Tribute webhook URL: http://%s:%s%s',
- settings.TRIBUTE_WEBHOOK_HOST,
- settings.TRIBUTE_WEBHOOK_PORT,
- settings.TRIBUTE_WEBHOOK_PATH,
+ 'Tribute webhook URL: http://',
+ TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
+ TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
+ TRIBUTE_WEBHOOK_PATH=settings.TRIBUTE_WEBHOOK_PATH,
)
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
- '%s webhook URL: http://%s:%s%s',
- mulenpay_name,
- settings.TRIBUTE_WEBHOOK_HOST,
- settings.TRIBUTE_WEBHOOK_PORT,
- settings.MULENPAY_WEBHOOK_PATH,
+ 'webhook URL: http://',
+ mulenpay_name=mulenpay_name,
+ TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
+ TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
+ MULENPAY_WEBHOOK_PATH=settings.MULENPAY_WEBHOOK_PATH,
)
if settings.is_cryptobot_enabled():
logger.info(
- 'CryptoBot webhook URL: http://%s:%s%s',
- settings.TRIBUTE_WEBHOOK_HOST,
- settings.TRIBUTE_WEBHOOK_PORT,
- settings.CRYPTOBOT_WEBHOOK_PATH,
+ 'CryptoBot webhook URL: http://',
+ TRIBUTE_WEBHOOK_HOST=settings.TRIBUTE_WEBHOOK_HOST,
+ TRIBUTE_WEBHOOK_PORT=settings.TRIBUTE_WEBHOOK_PORT,
+ CRYPTOBOT_WEBHOOK_PATH=settings.CRYPTOBOT_WEBHOOK_PATH,
)
except Exception as e:
- logger.error(f'Ошибка запуска webhook сервера: {e}')
+ logger.error('Ошибка запуска webhook сервера', error=e)
raise
async def stop(self):
@@ -137,7 +135,7 @@ class WebhookServer:
logger.info('Webhook runner очищен')
except Exception as e:
- logger.error(f'Ошибка остановки webhook сервера: {e}')
+ logger.error('Ошибка остановки webhook сервера', error=e)
async def _options_handler(self, request: web.Request) -> web.Response:
return web.Response(
@@ -152,36 +150,27 @@ class WebhookServer:
async def _mulenpay_webhook_handler(self, request: web.Request) -> web.Response:
try:
mulenpay_name = settings.get_mulenpay_display_name()
- logger.info(
- '%s webhook: %s %s',
- mulenpay_name,
- request.method,
- request.path,
- )
- logger.info(
- '%s webhook headers: %s',
- mulenpay_name,
- dict(request.headers),
- )
+ logger.info('webhook', mulenpay_name=mulenpay_name, method=request.method, request_path=request.path)
+ logger.info('webhook headers', mulenpay_name=mulenpay_name, headers=dict(request.headers))
raw_body = await request.read()
if not raw_body:
- logger.warning('Пустой %s webhook', mulenpay_name)
+ logger.warning('Пустой webhook', mulenpay_name=mulenpay_name)
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
# Временно отключаем проверку подписи для отладки
# TODO: Включить обратно после настройки MulenPay
if not self._verify_mulenpay_signature(request, raw_body):
logger.warning(
- '%s webhook signature verification failed, but processing anyway for debugging',
- mulenpay_name,
+ 'webhook signature verification failed, but processing anyway for debugging',
+ mulenpay_name=mulenpay_name,
)
# return web.json_response({"status": "error", "reason": "invalid_signature"}, status=401)
try:
payload = json.loads(raw_body.decode('utf-8'))
except json.JSONDecodeError as error:
- logger.error(f'Ошибка парсинга {mulenpay_name} webhook: {error}')
+ logger.error('Ошибка парсинга webhook', mulenpay_name=mulenpay_name, error=error)
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
payment_service = PaymentService(self.bot)
@@ -196,10 +185,7 @@ class WebhookServer:
return web.json_response({'status': 'ok'}, status=200)
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
except Exception as error:
- logger.error(
- f'Ошибка обработки {mulenpay_name} webhook: {error}',
- exc_info=True,
- )
+ logger.error('Ошибка обработки webhook', mulenpay_name=mulenpay_name, error=error, exc_info=True)
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
finally:
try:
@@ -209,10 +195,7 @@ class WebhookServer:
except Exception as error:
mulenpay_name = settings.get_mulenpay_display_name()
- logger.error(
- f'Критическая ошибка {mulenpay_name} webhook: {error}',
- exc_info=True,
- )
+ logger.error('Критическая ошибка webhook', mulenpay_name=mulenpay_name, error=error, exc_info=True)
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(error)}, status=500)
@staticmethod
@@ -228,14 +211,14 @@ class WebhookServer:
secret_key = settings.MULENPAY_SECRET_KEY
display_name = settings.get_mulenpay_display_name()
if not secret_key:
- logger.error('%s secret key is not configured', display_name)
+ logger.error('secret key is not configured', display_name=display_name)
return False
# Логируем все заголовки для отладки
- logger.info('%s webhook headers for signature verification:', display_name)
+ logger.info('webhook headers for signature verification', display_name=display_name)
for header_name, header_value in request.headers.items():
if any(keyword in header_name.lower() for keyword in ['signature', 'sign', 'token', 'auth']):
- logger.info(f' {header_name}: {header_value}')
+ logger.info('log event', header_name=header_name, header_value=header_value)
signature = WebhookServer._extract_mulenpay_header(
request,
@@ -283,7 +266,7 @@ class WebhookServer:
if hmac.compare_digest(normalized_signature_no_padding, expected_urlsafe_base64_signature.rstrip('=')):
return True
- logger.error('Неверная подпись %s webhook', display_name)
+ logger.error('Неверная подпись webhook', display_name=display_name)
return False
authorization_header = request.headers.get('Authorization')
@@ -296,7 +279,7 @@ class WebhookServer:
if hmac.compare_digest(token, secret_key):
return True
- logger.error('Неверный %s токен %s webhook', scheme, display_name)
+ logger.error('Неверный токен webhook', scheme=scheme, display_name=display_name)
return False
if not value and hmac.compare_digest(token, secret_key):
@@ -319,13 +302,13 @@ class WebhookServer:
{key: value for key, value in request.headers.items() if 'authorization' not in key.lower()},
)
- logger.error('Отсутствует подпись %s webhook', display_name)
+ logger.error('Отсутствует подпись webhook', display_name=display_name)
return False
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
try:
- logger.info(f'Получен Tribute webhook: {request.method} {request.path}')
- logger.info(f'Headers: {dict(request.headers)}')
+ logger.info('Получен Tribute webhook', method=request.method, path=request.path)
+ logger.info('Headers', value=dict(request.headers))
raw_body = await request.read()
@@ -334,17 +317,17 @@ class WebhookServer:
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
payload = raw_body.decode('utf-8')
- logger.info(f'Payload: {payload}')
+ logger.info('Payload', payload=payload)
try:
webhook_data = json.loads(payload)
- logger.info(f'Распарсенные данные: {webhook_data}')
+ logger.info('Распарсенные данные', webhook_data=webhook_data)
except json.JSONDecodeError as e:
- logger.error(f'Ошибка парсинга JSON: {e}')
+ logger.error('Ошибка парсинга JSON', error=e)
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
signature = request.headers.get('trbt-signature')
- logger.info(f'Signature: {signature}')
+ logger.info('Signature', signature=signature)
if not signature:
logger.error('Отсутствует заголовок подписи Tribute webhook')
@@ -361,19 +344,19 @@ class WebhookServer:
result = await self.tribute_service.process_webhook(payload)
if result:
- logger.info(f'Tribute webhook обработан успешно: {result}')
+ logger.info('Tribute webhook обработан успешно', result=result)
return web.json_response({'status': 'ok', 'result': result}, status=200)
logger.error('Ошибка обработки Tribute webhook')
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
except Exception as e:
- logger.error(f'Критическая ошибка обработки Tribute webhook: {e}', exc_info=True)
+ logger.error('Критическая ошибка обработки Tribute webhook', error=e, exc_info=True)
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(e)}, status=500)
async def _cryptobot_webhook_handler(self, request: web.Request) -> web.Response:
try:
- logger.info(f'Получен CryptoBot webhook: {request.method} {request.path}')
- logger.info(f'Headers: {dict(request.headers)}')
+ logger.info('Получен CryptoBot webhook', method=request.method, path=request.path)
+ logger.info('Headers', value=dict(request.headers))
raw_body = await request.read()
@@ -382,17 +365,17 @@ class WebhookServer:
return web.json_response({'status': 'error', 'reason': 'empty_body'}, status=400)
payload = raw_body.decode('utf-8')
- logger.info(f'CryptoBot Payload: {payload}')
+ logger.info('CryptoBot Payload', payload=payload)
try:
webhook_data = json.loads(payload)
- logger.info(f'CryptoBot данные: {webhook_data}')
+ logger.info('CryptoBot данные', webhook_data=webhook_data)
except json.JSONDecodeError as e:
- logger.error(f'Ошибка парсинга CryptoBot JSON: {e}')
+ logger.error('Ошибка парсинга CryptoBot JSON', error=e)
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
signature = request.headers.get('Crypto-Pay-API-Signature')
- logger.info(f'CryptoBot Signature: {signature}')
+ logger.info('CryptoBot Signature', signature=signature)
if signature and settings.CRYPTOBOT_WEBHOOK_SECRET:
from app.external.cryptobot import CryptoBotService
@@ -417,7 +400,7 @@ class WebhookServer:
return web.json_response({'status': 'error', 'reason': 'processing_failed'}, status=400)
except Exception as e:
- logger.error(f'Критическая ошибка обработки CryptoBot webhook: {e}', exc_info=True)
+ logger.error('Критическая ошибка обработки CryptoBot webhook', error=e, exc_info=True)
return web.json_response({'status': 'error', 'reason': 'internal_error', 'message': str(e)}, status=500)
async def _health_check(self, request: web.Request) -> web.Response:
@@ -448,22 +431,22 @@ class WebhookServer:
- CUR_ID: ID валюты/платежной системы
"""
try:
- logger.info(f'Получен Freekassa webhook: {request.method} {request.path}')
+ logger.info('Получен Freekassa webhook', method=request.method, path=request.path)
# Получаем IP клиента
client_ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip()
if not client_ip:
client_ip = request.remote or 'unknown'
- logger.info(f'Freekassa webhook IP: {client_ip}')
+ logger.info('Freekassa webhook IP', client_ip=client_ip)
# Freekassa отправляет form-data
try:
form_data = await request.post()
except Exception as e:
- logger.error(f'Ошибка парсинга Freekassa form-data: {e}')
+ logger.error('Ошибка парсинга Freekassa form-data', error=e)
return web.Response(text='NO', status=400)
- logger.info(f'Freekassa webhook data: {dict(form_data)}')
+ logger.info('Freekassa webhook data', value=dict(form_data))
# Извлекаем параметры
merchant_id = int(form_data.get('MERCHANT_ID', 0))
@@ -496,12 +479,12 @@ class WebhookServer:
)
if success:
- logger.info(f'Freekassa webhook обработан успешно: order_id={order_id}')
+ logger.info('Freekassa webhook обработан успешно: order_id', order_id=order_id)
# Freekassa ожидает YES в ответе
return web.Response(text='YES', status=200)
- logger.error(f'Ошибка обработки Freekassa webhook: order_id={order_id}')
+ logger.error('Ошибка обработки Freekassa webhook: order_id', order_id=order_id)
return web.Response(text='NO', status=400)
except Exception as e:
- logger.error(f'Критическая ошибка обработки Freekassa webhook: {e}', exc_info=True)
+ logger.error('Критическая ошибка обработки Freekassa webhook', error=e, exc_info=True)
return web.Response(text='NO', status=500)
diff --git a/app/external/yookassa_webhook.py b/app/external/yookassa_webhook.py
index df2b8e8c..4d75be4a 100644
--- a/app/external/yookassa_webhook.py
+++ b/app/external/yookassa_webhook.py
@@ -2,7 +2,6 @@ from __future__ import annotations
import asyncio
import json
-import logging
from collections.abc import Iterable
from ipaddress import (
IPv4Address,
@@ -14,6 +13,7 @@ from ipaddress import (
)
from typing import TYPE_CHECKING, Union
+import structlog
from aiohttp import web
from app.config import settings
@@ -23,7 +23,7 @@ from app.database.database import AsyncSessionLocal
if TYPE_CHECKING:
from app.services.payment_service import PaymentService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
IPAddress = Union[IPv4Address, IPv6Address]
@@ -140,7 +140,7 @@ def _get_trusted_proxy_networks() -> tuple[IPNetwork, ...]:
try:
networks.append(ip_network(candidate, strict=False))
except ValueError:
- logger.warning('Неверная сеть доверенного прокси YooKassa: %s', candidate)
+ logger.warning('Неверная сеть доверенного прокси YooKassa', candidate=candidate)
cached_networks = tuple(networks)
_TRUSTED_PROXY_NETWORKS_CACHE = (raw_value, cached_networks)
@@ -206,8 +206,8 @@ class YooKassaWebhookHandler:
async def handle_webhook(self, request: web.Request) -> web.Response:
try:
- logger.info(f'📥 Получен YooKassa webhook: {request.method} {request.path}')
- logger.info(f'📋 Headers: {dict(request.headers)}')
+ logger.info('📥 Получен YooKassa webhook', method=request.method, path=request.path)
+ logger.info('📋 Headers', value=dict(request.headers))
header_ip_candidates = collect_yookassa_ip_candidates(
request.headers.get('X-Forwarded-For'),
@@ -221,8 +221,8 @@ class YooKassaWebhookHandler:
if client_ip is None:
logger.warning(
- '🚫 Не удалось определить IP-адрес отправителя YooKassa webhook. Кандидаты: %s',
- header_ip_candidates + ([request.remote] if request.remote else []),
+ '🚫 Не удалось определить IP-адрес отправителя YooKassa webhook. Кандидаты',
+ header_ip_candidates=header_ip_candidates + ([request.remote] if request.remote else []),
)
return web.Response(status=403, text='Forbidden')
@@ -234,7 +234,7 @@ class YooKassaWebhookHandler:
)
return web.Response(status=403, text='Forbidden')
- logger.info('🌐 IP-адрес YooKassa подтверждён: %s', client_ip)
+ logger.info('🌐 IP-адрес YooKassa подтверждён', client_ip=client_ip)
body = await request.text()
@@ -242,20 +242,20 @@ class YooKassaWebhookHandler:
logger.warning('⚠️ Получен пустой webhook от YooKassa')
return web.Response(status=400, text='Empty body')
- logger.info(f'📄 Body: {body}')
+ logger.info('📄 Body', body=body)
signature = request.headers.get('Signature') or request.headers.get('X-YooKassa-Signature')
if signature:
- logger.info('ℹ️ Получена подпись YooKassa: %s', signature)
+ logger.info('ℹ️ Получена подпись YooKassa', signature=signature)
try:
webhook_data = json.loads(body)
except json.JSONDecodeError as e:
- logger.error(f'❌ Ошибка парсинга JSON webhook YooKassa: {e}')
+ logger.error('❌ Ошибка парсинга JSON webhook YooKassa', error=e)
return web.Response(status=400, text='Invalid JSON')
- logger.info(f'📊 Обработка webhook YooKassa: {webhook_data.get("event", "unknown_event")}')
- logger.debug(f'🔍 Полные данные webhook: {webhook_data}')
+ logger.info('📊 Обработка webhook YooKassa', get=webhook_data.get('event', 'unknown_event'))
+ logger.debug('🔍 Полные данные webhook', webhook_data=webhook_data)
event_type = webhook_data.get('event')
if not event_type:
@@ -269,7 +269,7 @@ class YooKassaWebhookHandler:
return web.Response(status=400, text='No payment id')
if event_type not in YOOKASSA_ALLOWED_EVENTS:
- logger.info(f'ℹ️ Игнорируем событие YooKassa: {event_type}')
+ logger.info('ℹ️ Игнорируем событие YooKassa', event_type=event_type)
return web.Response(status=200, text='OK')
async with AsyncSessionLocal() as db:
@@ -286,7 +286,8 @@ class YooKassaWebhookHandler:
if existing_transaction and event_type == 'payment.succeeded':
logger.info(
- f'ℹ️ Платеж YooKassa {yookassa_payment_id} уже был обработан. Пропускаем дублирующий вебхук.'
+ 'ℹ️ Платеж YooKassa уже был обработан. Пропускаем дублирующий вебхук.',
+ yookassa_payment_id=yookassa_payment_id,
)
return web.Response(status=200, text='OK')
@@ -295,22 +296,26 @@ class YooKassaWebhookHandler:
if success:
await db.commit()
logger.info(
- f'✅ Успешно обработан webhook YooKassa: {event_type} для платежа {yookassa_payment_id}'
+ '✅ Успешно обработан webhook YooKassa: для платежа',
+ event_type=event_type,
+ yookassa_payment_id=yookassa_payment_id,
)
return web.Response(status=200, text='OK')
await db.rollback()
logger.error(
- f'❌ Ошибка обработки webhook YooKassa: {event_type} для платежа {yookassa_payment_id}'
+ '❌ Ошибка обработки webhook YooKassa: для платежа',
+ event_type=event_type,
+ yookassa_payment_id=yookassa_payment_id,
)
return web.Response(status=500, text='Processing error')
except Exception as e:
await db.rollback()
- logger.error(f'❌ Ошибка обработки webhook YooKassa: {e}', exc_info=True)
+ logger.error('❌ Ошибка обработки webhook YooKassa', error=e, exc_info=True)
return web.Response(status=500, text='Processing error')
except Exception as e:
- logger.error(f'❌ Критическая ошибка обработки webhook YooKassa: {e}', exc_info=True)
+ logger.error('❌ Критическая ошибка обработки webhook YooKassa', error=e, exc_info=True)
return web.Response(status=500, text='Internal server error')
def setup_routes(self, app: web.Application) -> None:
@@ -319,7 +324,7 @@ class YooKassaWebhookHandler:
app.router.add_get(webhook_path, self._get_handler)
app.router.add_options(webhook_path, self._options_handler)
- logger.info(f'✅ Настроен YooKassa webhook на пути: POST {webhook_path}')
+ logger.info('✅ Настроен YooKassa webhook на пути: POST', webhook_path=webhook_path)
async def _get_handler(self, request: web.Request) -> web.Response:
return web.json_response(
@@ -381,15 +386,15 @@ async def start_yookassa_webhook_server(payment_service: PaymentService) -> None
await site.start()
logger.info(
- '✅ YooKassa webhook сервер запущен на %s:%s',
- settings.YOOKASSA_WEBHOOK_HOST,
- settings.YOOKASSA_WEBHOOK_PORT,
+ '✅ YooKassa webhook сервер запущен на',
+ YOOKASSA_WEBHOOK_HOST=settings.YOOKASSA_WEBHOOK_HOST,
+ YOOKASSA_WEBHOOK_PORT=settings.YOOKASSA_WEBHOOK_PORT,
)
logger.info(
- '🎯 YooKassa webhook URL: http://%s:%s%s',
- settings.YOOKASSA_WEBHOOK_HOST,
- settings.YOOKASSA_WEBHOOK_PORT,
- settings.YOOKASSA_WEBHOOK_PATH,
+ '🎯 YooKassa webhook URL: http://',
+ YOOKASSA_WEBHOOK_HOST=settings.YOOKASSA_WEBHOOK_HOST,
+ YOOKASSA_WEBHOOK_PORT=settings.YOOKASSA_WEBHOOK_PORT,
+ YOOKASSA_WEBHOOK_PATH=settings.YOOKASSA_WEBHOOK_PATH,
)
try:
@@ -403,5 +408,5 @@ async def start_yookassa_webhook_server(payment_service: PaymentService) -> None
logger.info('✅ YooKassa webhook сервер остановлен')
except Exception as e:
- logger.error(f'❌ Ошибка запуска YooKassa webhook сервера: {e}', exc_info=True)
+ logger.error('❌ Ошибка запуска YooKassa webhook сервера', error=e, exc_info=True)
raise
diff --git a/app/handlers/admin/backup.py b/app/handlers/admin/backup.py
index 7953486d..bac029ae 100644
--- a/app/handlers/admin/backup.py
+++ b/app/handlers/admin/backup.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
@@ -12,7 +12,7 @@ from app.services.backup_service import backup_service
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class BackupStates(StatesGroup):
@@ -446,7 +446,7 @@ async def handle_backup_file_upload(message: types.Message, db_user: User, db: A
await state.clear()
except Exception as e:
- logger.error(f'Ошибка загрузки файла бекапа: {e}')
+ logger.error('Ошибка загрузки файла бекапа', error=e)
await message.answer(
f'❌ Ошибка загрузки файла: {e!s}',
reply_markup=InlineKeyboardMarkup(
diff --git a/app/handlers/admin/blacklist.py b/app/handlers/admin/blacklist.py
index 13121e15..f3724f43 100644
--- a/app/handlers/admin/blacklist.py
+++ b/app/handlers/admin/blacklist.py
@@ -2,8 +2,7 @@
Обработчики админ-панели для управления черным списком
"""
-import logging
-
+import structlog
from aiogram import types
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
@@ -14,7 +13,7 @@ from app.states import BlacklistStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -23,7 +22,7 @@ async def show_blacklist_settings(callback: types.CallbackQuery, db_user: User,
"""
Показывает настройки черного списка
"""
- logger.info(f'Вызван обработчик show_blacklist_settings для пользователя {callback.from_user.id}')
+ logger.info('Вызван обработчик show_blacklist_settings для пользователя', from_user_id=callback.from_user.id)
is_enabled = blacklist_service.is_blacklist_check_enabled()
github_url = blacklist_service.get_blacklist_github_url()
diff --git a/app/handlers/admin/blocked_users.py b/app/handlers/admin/blocked_users.py
index fa77457e..5407c7f6 100644
--- a/app/handlers/admin/blocked_users.py
+++ b/app/handlers/admin/blocked_users.py
@@ -5,11 +5,11 @@
и выполнять очистку БД и панели Remnawave.
"""
-import logging
from datetime import UTC, datetime
from enum import Enum
from typing import Any
+import structlog
from aiogram import Bot, Dispatcher, F, types
from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
@@ -26,7 +26,7 @@ from app.services.blocked_users_service import (
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# =============================================================================
@@ -644,9 +644,11 @@ async def handle_confirm_action(
)
logger.info(
- f'Очистка заблокированных пользователей завершена: '
- f'DB={result.deleted_from_db}, RW={result.deleted_from_remnawave}, '
- f'marked={result.marked_as_blocked}, errors={len(result.errors)}'
+ 'Очистка заблокированных пользователей завершена: DB=, RW=, marked=, errors',
+ deleted_from_db=result.deleted_from_db,
+ deleted_from_remnawave=result.deleted_from_remnawave,
+ marked_as_blocked=result.marked_as_blocked,
+ errors_count=len(result.errors),
)
await callback.answer()
diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py
index d4dad256..454cc6ae 100644
--- a/app/handlers/admin/bot_configuration.py
+++ b/app/handlers/admin/bot_configuration.py
@@ -1,6 +1,5 @@
import html
import io
-import logging
import math
import time
from collections.abc import Iterable
@@ -693,17 +692,12 @@ async def apply_preset(
await bot_configuration_service.set_value(db, setting_key, value)
applied.append(setting_key)
except ReadOnlySettingError:
- logging.getLogger(__name__).info(
- 'Пропускаем настройку %s из пресета %s: только для чтения',
- setting_key,
- preset_key,
+ logger.info(
+ 'Пропускаем настройку из пресета : только для чтения', setting_key=setting_key, preset_key=preset_key
)
except Exception as error:
- logging.getLogger(__name__).warning(
- 'Не удалось применить пресет %s для %s: %s',
- preset_key,
- setting_key,
- error,
+ logger.warning(
+ 'Не удалось применить пресет для', preset_key=preset_key, setting_key=setting_key, error=error
)
await db.commit()
diff --git a/app/handlers/admin/bulk_ban.py b/app/handlers/admin/bulk_ban.py
index cb83c7bd..37b38245 100644
--- a/app/handlers/admin/bulk_ban.py
+++ b/app/handlers/admin/bulk_ban.py
@@ -2,8 +2,7 @@
Обработчики команд для массовой блокировки пользователей
"""
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +13,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -77,7 +76,7 @@ async def process_bulk_ban_list(message: types.Message, db_user: User, state: FS
try:
telegram_ids = await bulk_ban_service.parse_telegram_ids_from_text(input_text)
except Exception as e:
- logger.error(f'Ошибка парсинга Telegram ID: {e}')
+ logger.error('Ошибка парсинга Telegram ID', error=e)
await message.answer(
'❌ Ошибка при обработке списка ID. Проверьте формат ввода.',
reply_markup=types.InlineKeyboardMarkup(
@@ -143,7 +142,7 @@ async def process_bulk_ban_list(message: types.Message, db_user: User, state: FS
)
except Exception as e:
- logger.error(f'Ошибка при выполнении массовой блокировки: {e}')
+ logger.error('Ошибка при выполнении массовой блокировки', error=e)
await message.answer(
'❌ Произошла ошибка при выполнении массовой блокировки',
reply_markup=types.InlineKeyboardMarkup(
diff --git a/app/handlers/admin/campaigns.py b/app/handlers/admin/campaigns.py
index 701c87b3..07ab7090 100644
--- a/app/handlers/admin/campaigns.py
+++ b/app/handlers/admin/campaigns.py
@@ -1,6 +1,6 @@
-import logging
import re
+import structlog
from aiogram import Bot, Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -33,7 +33,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
_CAMPAIGN_PARAM_REGEX = re.compile(r'^[A-Za-z0-9_-]{3,32}$')
_CAMPAIGNS_PAGE_SIZE = 5
@@ -1120,7 +1120,7 @@ async def toggle_campaign_status(
new_status = not campaign.is_active
await update_campaign(db, campaign, is_active=new_status)
status_text = 'включена' if new_status else 'выключена'
- logger.info('🔄 Кампания %s переключена: %s', campaign_id, status_text)
+ logger.info('🔄 Кампания переключена', campaign_id=campaign_id, status_text=status_text)
await show_campaign_detail(callback, db_user, db)
diff --git a/app/handlers/admin/contests.py b/app/handlers/admin/contests.py
index f2e0af1a..0136fab6 100644
--- a/app/handlers/admin/contests.py
+++ b/app/handlers/admin/contests.py
@@ -1,8 +1,8 @@
-import logging
import math
from datetime import UTC, datetime, time
from zoneinfo import ZoneInfo
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -35,7 +35,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
PAGE_SIZE = 5
@@ -44,7 +44,7 @@ def _ensure_timezone(tz_name: str) -> ZoneInfo:
try:
return ZoneInfo(tz_name)
except Exception:
- logger.warning('Не удалось загрузить TZ %s, используем UTC', tz_name)
+ logger.warning('Не удалось загрузить TZ , используем UTC', tz_name=tz_name)
return ZoneInfo('UTC')
diff --git a/app/handlers/admin/daily_contests.py b/app/handlers/admin/daily_contests.py
index 7fc6c013..69032479 100644
--- a/app/handlers/admin/daily_contests.py
+++ b/app/handlers/admin/daily_contests.py
@@ -1,7 +1,7 @@
import json
-import logging
from datetime import datetime, timedelta
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -23,7 +23,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
EDITABLE_FIELDS: dict[str, dict] = {
'prize_type': {'type': str, 'label': 'тип приза (days/balance/custom)'},
diff --git a/app/handlers/admin/faq.py b/app/handlers/admin/faq.py
index 884ebe27..5645469a 100644
--- a/app/handlers/admin/faq.py
+++ b/app/handlers/admin/faq.py
@@ -1,7 +1,7 @@
import html
-import logging
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +14,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.validators import get_html_help_text, validate_html_tags
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _format_timestamp(value: datetime | None) -> str:
@@ -389,11 +389,7 @@ async def process_new_faq_content(
content=content,
)
- logger.info(
- 'Админ %s создал страницу FAQ (%d символов)',
- db_user.telegram_id,
- len(content),
- )
+ logger.info('Админ создал страницу FAQ (символов)', telegram_id=db_user.telegram_id, content_count=len(content))
await state.clear()
diff --git a/app/handlers/admin/main.py b/app/handlers/admin/main.py
index 218602d0..1e2e9405 100644
--- a/app/handlers/admin/main.py
+++ b/app/handlers/admin/main.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.filters import Command
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
@@ -24,7 +23,7 @@ from app.services.support_settings_service import SupportSettingsService
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -52,7 +51,7 @@ async def show_admin_panel(callback: types.CallbackQuery, db_user: User, db: Asy
),
)
except Exception as e:
- logger.error(f'Не удалось получить статистику Remnawave для админ-панели: {e}')
+ logger.error('Не удалось получить статистику Remnawave для админ-панели', error=e)
await callback.message.edit_text(admin_text, reply_markup=get_admin_main_keyboard(db_user.language))
await callback.answer()
@@ -288,12 +287,14 @@ async def clear_rules_command(message: types.Message, db_user: User, db: AsyncSe
f'Теперь используются стандартные правила по умолчанию.'
)
- logger.info(f'Правила очищены командой администратором {db_user.telegram_id} ({db_user.full_name})')
+ logger.info(
+ 'Правила очищены командой администратором', telegram_id=db_user.telegram_id, full_name=db_user.full_name
+ )
else:
await message.reply('⚠️ Нет правил для очистки\n\nАктивные правила не найдены.')
except Exception as e:
- logger.error(f'Ошибка при очистке правил командой: {e}')
+ logger.error('Ошибка при очистке правил командой', error=e)
await message.reply(
'❌ Ошибка при очистке правил\n\n'
f'Произошла ошибка: {e!s}\n'
@@ -330,7 +331,7 @@ async def rules_stats_command(message: types.Message, db_user: User, db: AsyncSe
await message.reply(text)
except Exception as e:
- logger.error(f'Ошибка при получении статистики правил: {e}')
+ logger.error('Ошибка при получении статистики правил', error=e)
await message.reply(f'❌ Ошибка получения статистики\n\nПроизошла ошибка: {e!s}')
diff --git a/app/handlers/admin/maintenance.py b/app/handlers/admin/maintenance.py
index 0fa7c07a..7510a5e1 100644
--- a/app/handlers/admin/maintenance.py
+++ b/app/handlers/admin/maintenance.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
@@ -12,7 +11,7 @@ from app.services.maintenance_service import maintenance_service
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class MaintenanceStates(StatesGroup):
@@ -33,7 +32,7 @@ async def show_maintenance_panel(callback: types.CallbackQuery, db_user: User, d
rw_service = RemnaWaveService()
panel_status = await rw_service.get_panel_status_summary()
except Exception as e:
- logger.error(f'Ошибка получения статуса панели: {e}')
+ logger.error('Ошибка получения статуса панели', error=e)
panel_status = {'description': '❓ Не удалось проверить', 'has_issues': True}
status_emoji = '🔧' if status_info['is_active'] else '✅'
@@ -316,7 +315,7 @@ async def process_notification_message(message: types.Message, db_user: User, db
await message.answer('❌ Ошибка отправки уведомления')
except Exception as e:
- logger.error(f'Ошибка отправки ручного уведомления: {e}')
+ logger.error('Ошибка отправки ручного уведомления', error=e)
await message.answer(f'❌ Ошибка: {e!s}')
await state.clear()
diff --git a/app/handlers/admin/messages.py b/app/handlers/admin/messages.py
index 8c8db193..cd623ebc 100644
--- a/app/handlers/admin/messages.py
+++ b/app/handlers/admin/messages.py
@@ -1,8 +1,8 @@
import asyncio
import html
-import logging
from datetime import datetime, timedelta
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
from aiogram.fsm.context import FSMContext
@@ -48,7 +48,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.miniapp_buttons import BUTTON_KEY_TO_CABINET_PATH, build_miniapp_or_callback_button
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def safe_edit_or_send_text(callback: types.CallbackQuery, text: str, reply_markup=None, parse_mode: str = 'HTML'):
@@ -158,8 +158,7 @@ async def _persist_broadcast_result(
broadcast_history = await session.get(BroadcastHistory, broadcast_id)
if not broadcast_history:
logger.critical(
- 'Не удалось найти запись BroadcastHistory #%s для записи результатов',
- broadcast_id,
+ 'Не удалось найти запись BroadcastHistory # для записи результатов', broadcast_id=broadcast_id
)
return
@@ -170,35 +169,35 @@ async def _persist_broadcast_result(
await session.commit()
logger.info(
- 'Результаты рассылки сохранены (id=%s, sent=%d, failed=%d, status=%s)',
- broadcast_id,
- sent_count,
- failed_count,
- status,
+ 'Результаты рассылки сохранены (id sent failed status=)',
+ broadcast_id=broadcast_id,
+ sent_count=sent_count,
+ failed_count=failed_count,
+ status=status,
)
return
except InterfaceError as error:
logger.warning(
- 'Ошибка соединения при сохранении результатов рассылки (попытка %d/%d): %s',
- attempt,
- max_retries,
- error,
+ 'Ошибка соединения при сохранении результатов рассылки (попытка /)',
+ attempt=attempt,
+ max_retries=max_retries,
+ error=error,
)
if attempt < max_retries:
await asyncio.sleep(retry_delay)
retry_delay *= 2
else:
logger.critical(
- 'Не удалось сохранить результаты рассылки после %d попыток (id=%s)',
- max_retries,
- broadcast_id,
+ 'Не удалось сохранить результаты рассылки после попыток (id=)',
+ max_retries=max_retries,
+ broadcast_id=broadcast_id,
)
except Exception as error:
logger.critical(
- 'Неожиданная ошибка при сохранении результатов рассылки (id=%s)',
- broadcast_id,
+ 'Неожиданная ошибка при сохранении результатов рассылки (id=)',
+ broadcast_id=broadcast_id,
exc_info=error,
)
return
@@ -1268,11 +1267,11 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
wait_seconds = e.retry_after + 1
flood_wait_until = asyncio.get_event_loop().time() + wait_seconds
logger.warning(
- 'FloodWait: Telegram просит подождать %d сек (пользователь %d, попытка %d/%d)',
- e.retry_after,
- telegram_id,
- attempt + 1,
- _MAX_SEND_RETRIES,
+ 'FloodWait: Telegram просит подождать сек (пользователь , попытка /)',
+ retry_after=e.retry_after,
+ telegram_id=telegram_id,
+ attempt=attempt + 1,
+ MAX_SEND_RETRIES=_MAX_SEND_RETRIES,
)
await asyncio.sleep(wait_seconds)
@@ -1280,16 +1279,16 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
return False
except TelegramBadRequest as e:
- logger.debug('BadRequest при рассылке пользователю %d: %s', telegram_id, e)
+ logger.debug('BadRequest при рассылке пользователю', telegram_id=telegram_id, e=e)
return False
except Exception as e:
logger.error(
- 'Ошибка отправки пользователю %d (попытка %d/%d): %s',
- telegram_id,
- attempt + 1,
- _MAX_SEND_RETRIES,
- e,
+ 'Ошибка отправки пользователю (попытка /)',
+ telegram_id=telegram_id,
+ attempt=attempt + 1,
+ MAX_SEND_RETRIES=_MAX_SEND_RETRIES,
+ e=e,
)
if attempt < _MAX_SEND_RETRIES - 1:
await asyncio.sleep(0.5 * (attempt + 1))
@@ -1341,7 +1340,7 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
await progress_message.edit_text(text, parse_mode='HTML')
except TelegramRetryAfter as e:
# Не паникуем — пропускаем обновление прогресса
- logger.debug('FloodWait при обновлении прогресса, пропускаем: %d сек', e.retry_after)
+ logger.debug('FloodWait при обновлении прогресса, пропускаем: сек', retry_after=e.retry_after)
except TelegramBadRequest:
# Сообщение удалено или контент не изменился — отправляем новое
try:
@@ -1378,7 +1377,7 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
failed_count += 1
elif isinstance(result, Exception):
failed_count += 1
- logger.error('Необработанное исключение в рассылке: %s', result)
+ logger.error('Необработанное исключение в рассылке', result=result)
# Обновляем прогресс каждые _PROGRESS_UPDATE_INTERVAL батчей
if batch_idx % _PROGRESS_UPDATE_INTERVAL == 0:
@@ -1390,7 +1389,7 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
# Учитываем пропущенных email-only пользователей
skipped_email_users = total_users_count - total_recipients
if skipped_email_users > 0:
- logger.info('Пропущено %d email-only пользователей при рассылке', skipped_email_users)
+ logger.info('Пропущено email-only пользователей при рассылке', skipped_email_users=skipped_email_users)
status = 'completed' if failed_count == 0 else 'partial'
@@ -1439,12 +1438,12 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
await state.clear()
logger.info(
- 'Рассылка завершена админом %s: sent=%d, failed=%d, total=%d (медиа: %s)',
- admin_telegram_id,
- sent_count,
- failed_count,
- total_users_count,
- has_media,
+ 'Рассылка завершена админом : sent failed total= (медиа:)',
+ admin_telegram_id=admin_telegram_id,
+ sent_count=sent_count,
+ failed_count=failed_count,
+ total_users_count=total_users_count,
+ has_media=has_media,
)
diff --git a/app/handlers/admin/monitoring.py b/app/handlers/admin/monitoring.py
index 48fb8426..6cd23fdd 100644
--- a/app/handlers/admin/monitoring.py
+++ b/app/handlers/admin/monitoring.py
@@ -1,7 +1,7 @@
import asyncio
-import logging
from datetime import datetime, timedelta
+import structlog
from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command
@@ -23,7 +23,7 @@ from app.utils.decorators import admin_required
from app.utils.pagination import paginate_list
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
router = Router()
@@ -393,7 +393,7 @@ async def admin_monitoring_menu(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка в админ меню мониторинга: {e}')
+ logger.error('Ошибка в админ меню мониторинга', error=e)
await callback.answer('❌ Ошибка получения данных', show_alert=True)
@@ -428,7 +428,7 @@ async def admin_monitoring_settings(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка отображения настроек мониторинга: {e}')
+ logger.error('Ошибка отображения настроек мониторинга', error=e)
await callback.answer('❌ Не удалось открыть настройки', show_alert=True)
@@ -438,7 +438,7 @@ async def admin_notify_settings(callback: CallbackQuery):
try:
await _render_notification_settings(callback)
except Exception as e:
- logger.error(f'Ошибка отображения настроек уведомлений: {e}')
+ logger.error('Ошибка отображения настроек уведомлений', error=e)
await callback.answer('❌ Не удалось загрузить настройки', show_alert=True)
@@ -459,7 +459,7 @@ async def preview_trial_channel_notification(callback: CallbackQuery):
await _send_notification_preview(callback.bot, callback.from_user.id, language, 'trial_channel_unsubscribed')
await callback.answer('✅ Пример отправлен')
except Exception as exc:
- logger.error('Failed to send trial channel preview: %s', exc)
+ logger.error('Failed to send trial channel preview', exc=exc)
await callback.answer('❌ Не удалось отправить тест', show_alert=True)
@@ -480,7 +480,7 @@ async def preview_expired_1d_notification(callback: CallbackQuery):
await _send_notification_preview(callback.bot, callback.from_user.id, language, 'expired_1d')
await callback.answer('✅ Пример отправлен')
except Exception as exc:
- logger.error('Failed to send expired 1d preview: %s', exc)
+ logger.error('Failed to send expired 1d preview', exc=exc)
await callback.answer('❌ Не удалось отправить тест', show_alert=True)
@@ -501,7 +501,7 @@ async def preview_second_wave_notification(callback: CallbackQuery):
await _send_notification_preview(callback.bot, callback.from_user.id, language, 'expired_2d')
await callback.answer('✅ Пример отправлен')
except Exception as exc:
- logger.error('Failed to send second wave preview: %s', exc)
+ logger.error('Failed to send second wave preview', exc=exc)
await callback.answer('❌ Не удалось отправить тест', show_alert=True)
@@ -522,7 +522,7 @@ async def preview_third_wave_notification(callback: CallbackQuery):
await _send_notification_preview(callback.bot, callback.from_user.id, language, 'expired_nd')
await callback.answer('✅ Пример отправлен')
except Exception as exc:
- logger.error('Failed to send third wave preview: %s', exc)
+ logger.error('Failed to send third wave preview', exc=exc)
await callback.answer('❌ Не удалось отправить тест', show_alert=True)
@@ -541,7 +541,7 @@ async def preview_all_notifications(callback: CallbackQuery):
await _send_notification_preview(callback.bot, chat_id, language, notification_type)
await callback.answer('✅ Все тестовые уведомления отправлены')
except Exception as exc:
- logger.error('Failed to send all notification previews: %s', exc)
+ logger.error('Failed to send all notification previews', exc=exc)
await callback.answer('❌ Не удалось отправить тесты', show_alert=True)
@@ -655,7 +655,7 @@ async def start_monitoring_callback(callback: CallbackQuery):
await admin_monitoring_menu(callback)
except Exception as e:
- logger.error(f'Ошибка запуска мониторинга: {e}')
+ logger.error('Ошибка запуска мониторинга', error=e)
await callback.answer(f'❌ Ошибка запуска: {e!s}', show_alert=True)
@@ -673,7 +673,7 @@ async def stop_monitoring_callback(callback: CallbackQuery):
await admin_monitoring_menu(callback)
except Exception as e:
- logger.error(f'Ошибка остановки мониторинга: {e}')
+ logger.error('Ошибка остановки мониторинга', error=e)
await callback.answer(f'❌ Ошибка остановки: {e!s}', show_alert=True)
@@ -708,7 +708,7 @@ async def force_check_callback(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка принудительной проверки: {e}')
+ logger.error('Ошибка принудительной проверки', error=e)
await callback.answer(f'❌ Ошибка проверки: {e!s}', show_alert=True)
@@ -774,7 +774,7 @@ async def traffic_check_callback(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка проверки трафика: {e}')
+ logger.error('Ошибка проверки трафика', error=e)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -826,7 +826,7 @@ async def monitoring_logs_callback(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка получения логов: {e}')
+ logger.error('Ошибка получения логов', error=e)
await callback.answer('❌ Ошибка получения логов', show_alert=True)
@@ -846,7 +846,7 @@ async def clear_logs_callback(callback: CallbackQuery):
await monitoring_logs_callback(callback)
except Exception as e:
- logger.error(f'Ошибка очистки логов: {e}')
+ logger.error('Ошибка очистки логов', error=e)
await callback.answer(f'❌ Ошибка очистки: {e!s}', show_alert=True)
@@ -872,7 +872,7 @@ async def test_notifications_callback(callback: CallbackQuery):
await callback.answer('✅ Тестовое уведомление отправлено!')
except Exception as e:
- logger.error(f'Ошибка отправки тестового уведомления: {e}')
+ logger.error('Ошибка отправки тестового уведомления', error=e)
await callback.answer(f'❌ Ошибка отправки: {e!s}', show_alert=True)
@@ -971,7 +971,7 @@ async def monitoring_statistics_callback(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка получения статистики: {e}')
+ logger.error('Ошибка получения статистики', error=e)
await callback.answer(f'❌ Ошибка получения статистики: {e!s}', show_alert=True)
@@ -1082,7 +1082,7 @@ async def nalogo_force_process_callback(callback: CallbackQuery):
await callback.message.edit_text(stats_text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка принудительной обработки чеков: {e}')
+ logger.error('Ошибка принудительной обработки чеков', error=e)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1143,7 +1143,7 @@ async def nalogo_pending_callback(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка просмотра очереди проверки: {e}')
+ logger.error('Ошибка просмотра очереди проверки', error=e)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1168,7 +1168,7 @@ async def nalogo_mark_verified_callback(callback: CallbackQuery):
await callback.answer('❌ Чек не найден', show_alert=True)
except Exception as e:
- logger.error(f'Ошибка пометки чека: {e}')
+ logger.error('Ошибка пометки чека', error=e)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1194,7 +1194,7 @@ async def nalogo_retry_callback(callback: CallbackQuery):
await callback.answer('❌ Не удалось создать чек', show_alert=True)
except Exception as e:
- logger.error(f'Ошибка повторной отправки чека: {e}')
+ logger.error('Ошибка повторной отправки чека', error=e)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1218,7 +1218,7 @@ async def nalogo_clear_pending_callback(callback: CallbackQuery):
)
except Exception as e:
- logger.error(f'Ошибка очистки очереди: {e}')
+ logger.error('Ошибка очистки очереди', error=e)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1334,7 +1334,7 @@ async def receipts_link_old_callback(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка привязки старых чеков: {e}', exc_info=True)
+ logger.error('Ошибка привязки старых чеков', error=e, exc_info=True)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1415,7 +1415,7 @@ async def _do_reconcile_logs(callback: CallbackQuery):
date_str, payment_id, receipt_uuid = match.groups()
receipts[payment_id] = {'date': date_str, 'receipt_uuid': receipt_uuid}
except Exception as e:
- logger.error(f'Ошибка чтения логов: {e}')
+ logger.error('Ошибка чтения логов', error=e)
await callback.message.edit_text(
f'❌ Ошибка чтения логов\n\n{e!s}',
parse_mode='HTML',
@@ -1479,7 +1479,7 @@ async def _do_reconcile_logs(callback: CallbackQuery):
except TelegramBadRequest:
pass # Игнорируем если сообщение не изменилось
except Exception as e:
- logger.error(f'Ошибка сверки по логам: {e}', exc_info=True)
+ logger.error('Ошибка сверки по логам', error=e, exc_info=True)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1575,7 +1575,7 @@ async def receipts_reconcile_logs_details_callback(callback: CallbackQuery):
except TelegramBadRequest:
pass
except Exception as e:
- logger.error(f'Ошибка детализации: {e}', exc_info=True)
+ logger.error('Ошибка детализации', error=e, exc_info=True)
await callback.answer(f'❌ Ошибка: {e!s}', show_alert=True)
@@ -1647,7 +1647,7 @@ async def monitoring_command(message: Message):
await message.answer(text, parse_mode='HTML')
except Exception as e:
- logger.error(f'Ошибка команды /monitoring: {e}')
+ logger.error('Ошибка команды /monitoring', error=e)
await message.answer(f'❌ Ошибка: {e!s}')
@@ -1836,7 +1836,7 @@ async def admin_traffic_settings(callback: CallbackQuery):
keyboard = _build_traffic_settings_keyboard()
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка отображения настроек трафика: {e}')
+ logger.error('Ошибка отображения настроек трафика', error=e)
await callback.answer('❌ Ошибка загрузки настроек', show_alert=True)
@@ -1862,7 +1862,7 @@ async def toggle_fast_check(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка переключения быстрой проверки: {e}')
+ logger.error('Ошибка переключения быстрой проверки', error=e)
await callback.answer('❌ Ошибка', show_alert=True)
@@ -1887,7 +1887,7 @@ async def toggle_daily_check(callback: CallbackQuery):
await callback.message.edit_text(text, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка переключения суточной проверки: {e}')
+ logger.error('Ошибка переключения суточной проверки', error=e)
await callback.answer('❌ Ошибка', show_alert=True)
@@ -2037,7 +2037,7 @@ async def process_traffic_setting_input(message: Message, state: FSMContext):
pass # Игнорируем если сообщение уже удалено
except Exception as e:
- logger.error(f'Ошибка сохранения настройки трафика: {e}')
+ logger.error('Ошибка сохранения настройки трафика', error=e)
await message.answer(f'❌ Ошибка сохранения: {e!s}')
await state.clear()
diff --git a/app/handlers/admin/payments.py b/app/handlers/admin/payments.py
index 950e6a37..682ce7a1 100644
--- a/app/handlers/admin/payments.py
+++ b/app/handlers/admin/payments.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import html
import math
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,6 +23,8 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_time_ago, format_username
+logger = structlog.get_logger(__name__)
+
PAGE_SIZE = 6
@@ -603,32 +606,29 @@ async def manual_check_payment(
db_user: User,
db: AsyncSession,
) -> None:
- import logging
-
- logger = logging.getLogger(__name__)
- logger.info('manual_check_payment called: %s', callback.data)
+ logger.info('manual_check_payment called', callback_data=callback.data)
parsed = _parse_method_and_id(callback.data, prefix='admin_payment_check_')
if not parsed:
- logger.warning('Failed to parse: %s', callback.data)
+ logger.warning('Failed to parse', callback_data=callback.data)
await callback.answer('❌ Invalid payment reference', show_alert=True)
return
method, payment_id = parsed
- logger.info('Checking payment: method=%s, id=%s', method, payment_id)
+ logger.info('Checking payment: method id', method=method, payment_id=payment_id)
record = await get_payment_record(db, method, payment_id)
texts = get_texts(db_user.language)
if not record:
- logger.warning('Payment not found: method=%s, id=%s', method, payment_id)
+ logger.warning('Payment not found: method id', method=method, payment_id=payment_id)
await callback.answer(texts.t('ADMIN_PAYMENT_NOT_FOUND', 'Payment not found.'), show_alert=True)
return
- logger.info('Record found: status=%s, is_paid=%s', record.status, record.is_paid)
+ logger.info('Record found: status is_paid', record_status=record.status, is_paid=record.is_paid)
if not _is_checkable(record):
- logger.info('Payment not checkable: method=%s, status=%s', method, record.status)
+ logger.info('Payment not checkable: method status', method=method, record_status=record.status)
await callback.answer(
texts.t('ADMIN_PAYMENT_CHECK_NOT_AVAILABLE', 'Manual check is not available for this invoice.'),
show_alert=True,
@@ -638,7 +638,7 @@ async def manual_check_payment(
logger.info('Running manual check...')
payment_service = PaymentService(callback.bot)
updated = await run_manual_check(db, method, payment_id, payment_service)
- logger.info('Check result: updated=%s', updated is not None)
+ logger.info('Check result', updated=updated is not None)
if not updated:
await callback.answer(
@@ -672,19 +672,16 @@ async def check_all_payments(
db: AsyncSession,
) -> None:
"""Массовая проверка всех ожидающих платежей."""
- import logging
-
- logger = logging.getLogger(__name__)
logger.info('check_all_payments called')
texts = get_texts(db_user.language)
# Получаем все ожидающие платежи
records = await list_recent_pending_payments(db)
- logger.info('Found %d total records', len(records))
+ logger.info('Found total records', records_count=len(records))
checkable_records = [r for r in records if _is_checkable(r) and not r.is_paid]
- logger.info('Found %d checkable records', len(checkable_records))
+ logger.info('Found checkable records', checkable_records_count=len(checkable_records))
if not checkable_records:
await callback.answer(
@@ -704,17 +701,17 @@ async def check_all_payments(
for record in checkable_records:
try:
- logger.info('Checking %s payment id=%s', record.method.value, record.local_id)
+ logger.info('Checking payment', method=record.method.value, local_id=record.local_id)
updated = await run_manual_check(db, record.method, record.local_id, payment_service)
checked += 1
- logger.info('Check result: is_paid=%s', updated.is_paid if updated else None)
+ logger.info('Check result: is_paid', is_paid=updated.is_paid if updated else None)
if updated and updated.is_paid and not record.is_paid:
confirmed += 1
except Exception as e:
- logger.error('Check failed for %s id=%s: %s', record.method.value, record.local_id, e, exc_info=True)
+ logger.error('Check failed', method=record.method.value, local_id=record.local_id, error=e, exc_info=True)
failed += 1
- logger.info('Check complete: checked=%d, confirmed=%d, failed=%d', checked, confirmed, failed)
+ logger.info('Check complete: checked confirmed failed', checked=checked, confirmed=confirmed, failed=failed)
# Показываем результат
result_lines = [
@@ -759,7 +756,7 @@ async def check_all_payments(
)
logger.info('Message updated successfully')
except Exception as e:
- logger.error('Failed to update message: %s', e, exc_info=True)
+ logger.error('Failed to update message', e=e, exc_info=True)
@admin_required
diff --git a/app/handlers/admin/polls.py b/app/handlers/admin/polls.py
index 51497909..d51000b9 100644
--- a/app/handlers/admin/polls.py
+++ b/app/handlers/admin/polls.py
@@ -1,7 +1,7 @@
import html
-import logging
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
+import structlog
from aiogram import Bot, Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -31,14 +31,14 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.validators import get_html_help_text, validate_html_tags
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _safe_format_price(amount_kopeks: int) -> str:
try:
return settings.format_price(amount_kopeks)
except Exception as error: # pragma: no cover - defensive logging
- logger.error('Не удалось отформатировать сумму %s: %s', amount_kopeks, error)
+ logger.error('Не удалось отформатировать сумму', amount_kopeks=amount_kopeks, error=error)
return f'{amount_kopeks / 100:.2f} ₽'
@@ -47,9 +47,9 @@ async def _safe_delete_message(message: types.Message) -> None:
await message.delete()
except TelegramBadRequest as error:
if 'message to delete not found' in str(error).lower():
- logger.debug('Сообщение уже удалено: %s', error)
+ logger.debug('Сообщение уже удалено', error=error)
else:
- logger.warning('Не удалось удалить сообщение %s: %s', message.message_id, error)
+ logger.warning('Не удалось удалить сообщение', message_id=message.message_id, error=error)
async def _edit_creation_message(
@@ -87,9 +87,7 @@ async def _edit_creation_message(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- 'Непредвиденная ошибка при обновлении сообщения создания опроса %s: %s',
- message_id,
- error,
+ 'Непредвиденная ошибка при обновлении сообщения создания опроса', message_id=message_id, error=error
)
return False
@@ -112,18 +110,12 @@ async def _send_creation_message(
except TelegramBadRequest as error:
error_text = str(error).lower()
if 'message to delete not found' in error_text:
- logger.debug('Сообщение уже удалено: %s', error)
+ logger.debug('Сообщение уже удалено', error=error)
else:
- logger.warning(
- 'Не удалось удалить сообщение создания опроса %s: %s',
- message_id,
- error,
- )
+ logger.warning('Не удалось удалить сообщение создания опроса', message_id=message_id, error=error)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- 'Непредвиденная ошибка при удалении сообщения создания опроса %s: %s',
- message_id,
- error,
+ 'Непредвиденная ошибка при удалении сообщения создания опроса', message_id=message_id, error=error
)
sent_message = await message.bot.send_message(
diff --git a/app/handlers/admin/pricing.py b/app/handlers/admin/pricing.py
index f9f95462..d6d95666 100644
--- a/app/handlers/admin/pricing.py
+++ b/app/handlers/admin/pricing.py
@@ -1,9 +1,9 @@
-import logging
from collections.abc import Iterable
from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from typing import Any
+import structlog
from aiogram import Bot, Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -17,7 +17,7 @@ from app.states import PricingStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
PriceItem = tuple[str, str, int]
@@ -825,7 +825,7 @@ async def _render_message(
try:
await message.edit_text(text, reply_markup=keyboard, parse_mode='HTML')
except TelegramBadRequest as error: # message changed elsewhere
- logger.debug('Failed to edit pricing message: %s', error)
+ logger.debug('Failed to edit pricing message', error=error)
await message.answer(text, reply_markup=keyboard, parse_mode='HTML')
@@ -845,7 +845,7 @@ async def _render_message_by_id(
parse_mode='HTML',
)
except TelegramBadRequest as error:
- logger.debug('Failed to edit pricing message by id: %s', error)
+ logger.debug('Failed to edit pricing message by id', error=error)
await bot.send_message(chat_id, text, reply_markup=keyboard, parse_mode='HTML')
@@ -1136,7 +1136,7 @@ async def process_pricing_input(
try:
await message.delete()
except TelegramBadRequest as error:
- logger.debug('Failed to delete pricing input message: %s', error)
+ logger.debug('Failed to delete pricing input message', error=error)
await state.clear()
return
entry = SETTING_ENTRY_BY_KEY.get(key)
diff --git a/app/handlers/admin/privacy_policy.py b/app/handlers/admin/privacy_policy.py
index 92d9bd44..a4f57cb3 100644
--- a/app/handlers/admin/privacy_policy.py
+++ b/app/handlers/admin/privacy_policy.py
@@ -1,7 +1,7 @@
import html
-import logging
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +14,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.validators import get_html_help_text, validate_html_tags
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _format_timestamp(value: datetime | None) -> str:
@@ -344,9 +344,9 @@ async def process_privacy_policy_edit(
await PrivacyPolicyService.save_policy(db, db_user.language, new_text)
logger.info(
- 'Админ %s обновил текст политики конфиденциальности (%d символов)',
- db_user.telegram_id,
- len(new_text),
+ 'Админ обновил текст политики конфиденциальности (символов)',
+ telegram_id=db_user.telegram_id,
+ new_text_count=len(new_text),
)
await state.clear()
diff --git a/app/handlers/admin/promo_groups.py b/app/handlers/admin/promo_groups.py
index dd86b067..74260f40 100644
--- a/app/handlers/admin/promo_groups.py
+++ b/app/handlers/admin/promo_groups.py
@@ -1,6 +1,6 @@
-import logging
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -27,7 +27,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.pricing_utils import format_period_description
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _format_discount_lines(texts, group) -> list[str]:
@@ -806,7 +806,7 @@ async def process_create_group_auto_assign(
auto_assign_total_spent_kopeks=auto_assign_kopeks,
)
except Exception as e:
- logger.error(f'Не удалось создать промогруппу: {e}')
+ logger.error('Не удалось создать промогруппу', error=e)
await message.answer(texts.ERROR)
await state.clear()
return
diff --git a/app/handlers/admin/promo_offers.py b/app/handlers/admin/promo_offers.py
index 33c187b7..38ea47eb 100644
--- a/app/handlers/admin/promo_offers.py
+++ b/app/handlers/admin/promo_offers.py
@@ -2,11 +2,11 @@ from __future__ import annotations
import asyncio
import html
-import logging
import re
from collections.abc import Sequence
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.fsm.context import FSMContext
@@ -48,7 +48,7 @@ from app.utils.miniapp_buttons import build_miniapp_or_callback_button
from app.utils.subscription_utils import get_display_subscription_link
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
SQUADS_PAGE_LIMIT = 10
@@ -61,7 +61,7 @@ async def _safe_delete_message(message: Message) -> None:
await message.delete()
except TelegramBadRequest as exc:
if 'message to delete not found' not in str(exc).lower():
- logger.debug('Не удалось удалить сообщение администратора: %s', exc)
+ logger.debug('Не удалось удалить сообщение администратора', exc=exc)
except TelegramForbiddenError:
logger.debug('Недостаточно прав для удаления сообщения администратора')
@@ -72,16 +72,11 @@ async def _safe_delete_message_by_id(bot, chat_id: int, message_id: int) -> None
except TelegramBadRequest as exc:
if 'message to delete not found' not in str(exc).lower():
logger.debug(
- 'Не удалось удалить сообщение администратора (%s, %s): %s',
- chat_id,
- message_id,
- exc,
+ 'Не удалось удалить сообщение администратора (,)', chat_id=chat_id, message_id=message_id, exc=exc
)
except TelegramForbiddenError:
logger.debug(
- 'Недостаточно прав для удаления сообщения администратора (%s, %s)',
- chat_id,
- message_id,
+ 'Недостаточно прав для удаления сообщения администратора (,)', chat_id=chat_id, message_id=message_id
)
@@ -168,7 +163,7 @@ def _render_template_text(
try:
return template.message_text.format(**replacements)
except Exception: # pragma: no cover - fallback for invalid placeholders
- logger.warning('Не удалось форматировать текст промо-предложения %s', template.id)
+ logger.warning('Не удалось форматировать текст промо-предложения', template_id=template.id)
return template.message_text
@@ -1181,7 +1176,7 @@ async def _handle_edit_field(
except TelegramBadRequest:
logger.debug('Не удалось удалить сообщение промо без текста')
else:
- logger.warning('Не удалось обновить сообщение редактирования промо: %s', exc)
+ logger.warning('Не удалось обновить сообщение редактирования промо', exc=exc)
await message.answer(description, reply_markup=reply_markup, parse_mode='HTML')
else:
await message.answer(description, reply_markup=reply_markup, parse_mode='HTML')
@@ -1928,7 +1923,7 @@ async def _send_offer_to_users(
"""Отправляет одно предложение с семафором ограничения"""
# Skip email-only users (no telegram_id)
if not user.telegram_id:
- logger.debug('Пропуск email-пользователя %s при рассылке промо', user.id)
+ logger.debug('Пропуск email-пользователя при рассылке промо', user_id=user.id)
return False
async with semaphore:
@@ -1989,10 +1984,14 @@ async def _send_offer_to_users(
)
return True
except (TelegramForbiddenError, TelegramBadRequest) as exc:
- logger.warning('Не удалось отправить предложение пользователю %s: %s', user.telegram_id or user.id, exc)
+ logger.warning(
+ 'Не удалось отправить предложение пользователю', telegram_id=user.telegram_id or user.id, exc=exc
+ )
return False
except Exception as exc: # pragma: no cover - defensive logging
- logger.error('Ошибка рассылки промо предложения пользователю %s: %s', user.telegram_id or user.id, exc)
+ logger.error(
+ 'Ошибка рассылки промо предложения пользователю', telegram_id=user.telegram_id or user.id, exc=exc
+ )
return False
# Отправляем предложения пакетами для эффективности
diff --git a/app/handlers/admin/promocodes.py b/app/handlers/admin/promocodes.py
index 62390a5b..15ee323c 100644
--- a/app/handlers/admin/promocodes.py
+++ b/app/handlers/admin/promocodes.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime, timedelta
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -28,7 +28,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -597,7 +597,11 @@ async def handle_edit_value(message: types.Message, db_user: User, state: FSMCon
await state.clear()
logger.info(
- f'Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: {edit_action} = {value}'
+ 'Промокод отредактирован администратором',
+ code=promo.code,
+ telegram_id=db_user.telegram_id,
+ edit_action=edit_action,
+ value=value,
)
except ValueError:
@@ -672,7 +676,10 @@ async def handle_edit_uses(message: types.Message, db_user: User, state: FSMCont
await state.clear()
logger.info(
- f'Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: max_uses = {max_uses}'
+ 'Промокод отредактирован администратором max_uses',
+ code=promo.code,
+ telegram_id=db_user.telegram_id,
+ max_uses=max_uses,
)
except ValueError:
@@ -770,7 +777,7 @@ async def process_promocode_expiry(message: types.Message, db_user: User, state:
)
await state.clear()
- logger.info(f'Создан промокод {code} администратором {db_user.telegram_id}')
+ logger.info('Создан промокод администратором', code=code, telegram_id=db_user.telegram_id)
except ValueError:
await message.answer('❌ Введите корректное число дней')
@@ -840,7 +847,11 @@ async def process_discount_hours(message: types.Message, db_user: User, state: F
await state.clear()
logger.info(
- f'Создан DISCOUNT промокод {code} ({value}%, {discount_hours}ч) администратором {db_user.telegram_id}'
+ 'Создан DISCOUNT промокод (%, ч) администратором',
+ code=code,
+ value=value,
+ discount_hours=discount_hours,
+ telegram_id=db_user.telegram_id,
)
except ValueError:
@@ -886,7 +897,10 @@ async def handle_edit_expiry(message: types.Message, db_user: User, state: FSMCo
await state.clear()
logger.info(
- f'Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: expiry = {expiry_days} дней'
+ 'Промокод отредактирован администратором expiry дней',
+ code=promo.code,
+ telegram_id=db_user.telegram_id,
+ expiry_days=expiry_days,
)
except ValueError:
diff --git a/app/handlers/admin/public_offer.py b/app/handlers/admin/public_offer.py
index 31043965..792584ca 100644
--- a/app/handlers/admin/public_offer.py
+++ b/app/handlers/admin/public_offer.py
@@ -1,7 +1,7 @@
import html
-import logging
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +14,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.validators import get_html_help_text, validate_html_tags
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _format_timestamp(value: datetime | None) -> str:
@@ -349,9 +349,7 @@ async def process_public_offer_edit(
await PublicOfferService.save_offer(db, db_user.language, new_text)
logger.info(
- 'Админ %s обновил текст публичной оферты (%d символов)',
- db_user.telegram_id,
- len(new_text),
+ 'Админ обновил текст публичной оферты (символов)', telegram_id=db_user.telegram_id, new_text_count=len(new_text)
)
await state.clear()
diff --git a/app/handlers/admin/referrals.py b/app/handlers/admin/referrals.py
index 0ba96478..e854d277 100644
--- a/app/handlers/admin/referrals.py
+++ b/app/handlers/admin/referrals.py
@@ -1,7 +1,7 @@
import datetime
import json
-import logging
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy import select
@@ -20,7 +20,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -64,7 +64,7 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User,
if count > 0:
text += f'{i}. ID {user_id}: {settings.format_price(earned)} ({count} реф.)\n'
else:
- logger.warning(f'Реферер {user_id} имеет {count} рефералов, но есть в топе')
+ logger.warning('Реферер имеет рефералов, но есть в топе', user_id=user_id, count=count)
else:
text += 'Нет данных\n'
@@ -108,11 +108,11 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User,
if 'message is not modified' in str(edit_error):
await callback.answer('Данные актуальны')
else:
- logger.error(f'Ошибка редактирования сообщения: {edit_error}')
+ logger.error('Ошибка редактирования сообщения', edit_error=edit_error)
await callback.answer('Ошибка обновления')
except Exception as e:
- logger.error(f'Ошибка в show_referral_statistics: {e}', exc_info=True)
+ logger.error('Ошибка в show_referral_statistics', error=e, exc_info=True)
current_time = datetime.datetime.now().strftime('%H:%M:%S')
text = f"""
@@ -253,7 +253,7 @@ async def _show_top_referrers_filtered(callback: types.CallbackQuery, db: AsyncS
raise
except Exception as e:
- logger.error(f'Ошибка в show_top_referrers_filtered: {e}', exc_info=True)
+ logger.error('Ошибка в show_top_referrers_filtered', error=e, exc_info=True)
await callback.answer('Ошибка загрузки топа рефереров')
@@ -448,7 +448,7 @@ async def approve_withdrawal_request(callback: types.CallbackQuery, db_user: Use
).format(id=request.id, amount=texts.format_price(request.amount_kopeks)),
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления пользователю: {e}')
+ logger.error('Ошибка отправки уведомления пользователю', error=e)
await callback.answer('✅ Заявка одобрена, средства списаны с баланса')
@@ -489,7 +489,7 @@ async def reject_withdrawal_request(callback: types.CallbackQuery, db_user: User
).format(id=request.id, amount=texts.format_price(request.amount_kopeks)),
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления пользователю: {e}')
+ logger.error('Ошибка отправки уведомления пользователю', error=e)
await callback.answer('❌ Заявка отклонена')
@@ -530,7 +530,7 @@ async def complete_withdrawal_request(callback: types.CallbackQuery, db_user: Us
).format(id=request.id, amount=texts.format_price(request.amount_kopeks)),
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления пользователю: {e}')
+ logger.error('Ошибка отправки уведомления пользователю', error=e)
await callback.answer('✅ Заявка выполнена')
@@ -647,7 +647,10 @@ async def process_test_referral_earning(message: types.Message, db_user: User, d
)
logger.info(
- f'Тестовое начисление: админ {db_user.telegram_id} начислил {amount_rubles}₽ пользователю {target_telegram_id}'
+ 'Тестовое начисление: админ начислил ₽ пользователю',
+ telegram_id=db_user.telegram_id,
+ amount_rubles=amount_rubles,
+ target_telegram_id=target_telegram_id,
)
@@ -794,7 +797,7 @@ async def _show_diagnostics_for_period(callback: types.CallbackQuery, db: AsyncS
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка в _show_diagnostics_for_period: {e}', exc_info=True)
+ logger.error('Ошибка в _show_diagnostics_for_period', error=e, exc_info=True)
await callback.answer('Ошибка при анализе логов', show_alert=True)
@@ -899,7 +902,7 @@ async def preview_referral_fixes(callback: types.CallbackQuery, db_user: User, d
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка в preview_referral_fixes: {e}', exc_info=True)
+ logger.error('Ошибка в preview_referral_fixes', error=e, exc_info=True)
await callback.answer('Ошибка при создании предпросмотра', show_alert=True)
@@ -1002,7 +1005,7 @@ async def apply_referral_fixes(callback: types.CallbackQuery, db_user: User, db:
await state.update_data(uploaded_file_report=None)
except Exception as e:
- logger.error(f'Ошибка в apply_referral_fixes: {e}', exc_info=True)
+ logger.error('Ошибка в apply_referral_fixes', error=e, exc_info=True)
await callback.answer('Ошибка при применении исправлений', show_alert=True)
@@ -1075,7 +1078,7 @@ async def check_missing_bonuses(callback: types.CallbackQuery, db_user: User, db
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка в check_missing_bonuses: {e}', exc_info=True)
+ logger.error('Ошибка в check_missing_bonuses', error=e, exc_info=True)
await callback.answer('Ошибка при проверке бонусов', show_alert=True)
@@ -1134,7 +1137,7 @@ async def apply_missing_bonuses(callback: types.CallbackQuery, db_user: User, db
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка в apply_missing_bonuses: {e}', exc_info=True)
+ logger.error('Ошибка в apply_missing_bonuses', error=e, exc_info=True)
await callback.answer('Ошибка при начислении бонусов', show_alert=True)
@@ -1211,7 +1214,7 @@ async def sync_referrals_with_contest(
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
- logger.error(f'Ошибка в sync_referrals_with_contest: {e}', exc_info=True)
+ logger.error('Ошибка в sync_referrals_with_contest', error=e, exc_info=True)
await callback.answer('Ошибка при синхронизации', show_alert=True)
@@ -1306,7 +1309,7 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
file = await message.bot.get_file(message.document.file_id)
await message.bot.download_file(file.file_path, temp_file_path)
- logger.info(f'📥 Файл загружен: {temp_file_path} ({message.document.file_size} байт)')
+ logger.info('📥 Файл загружен: ( байт)', temp_file_path=temp_file_path, file_size=message.document.file_size)
# Обновляем статус
await status_message.edit_text(f'🔍 Анализирую файл {file_name}...\n\nЭто может занять некоторое время.')
@@ -1396,7 +1399,7 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
await state.set_state(AdminStates.referral_diagnostics_period)
except Exception as e:
- logger.error(f'❌ Ошибка при обработке файла: {e}', exc_info=True)
+ logger.error('❌ Ошибка при обработке файла', error=e, exc_info=True)
try:
await status_message.edit_text(
@@ -1434,9 +1437,9 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
if temp_file_path and Path(temp_file_path).exists():
try:
Path(temp_file_path).unlink()
- logger.info(f'🗑️ Временный файл удалён: {temp_file_path}')
+ logger.info('🗑️ Временный файл удалён', temp_file_path=temp_file_path)
except Exception as e:
- logger.error(f'Ошибка удаления временного файла: {e}')
+ logger.error('Ошибка удаления временного файла', error=e)
def register_handlers(dp: Dispatcher):
diff --git a/app/handlers/admin/remnawave.py b/app/handlers/admin/remnawave.py
index 868ce62e..d374e650 100644
--- a/app/handlers/admin/remnawave.py
+++ b/app/handlers/admin/remnawave.py
@@ -1,8 +1,8 @@
-import logging
import math
from datetime import datetime
from typing import Any
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -37,7 +37,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_bytes, format_datetime
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
squad_inbound_selections = {}
squad_create_data = {}
@@ -1403,7 +1403,7 @@ async def show_node_statistics(callback: types.CallbackQuery, db_user: User, db:
await callback.answer()
except Exception as e:
- logger.error(f'Ошибка получения статистики ноды {node_uuid}: {e}')
+ logger.error('Ошибка получения статистики ноды', node_uuid=node_uuid, error=e)
text = f"""
📊 Статистика ноды: {node['name']}
@@ -1842,7 +1842,7 @@ async def save_squad_inbounds(callback: types.CallbackQuery, db_user: User, db:
await callback.answer('❌ Ошибка сохранения изменений', show_alert=True)
except Exception as e:
- logger.error(f'Error saving squad inbounds: {e}')
+ logger.error('Error saving squad inbounds', error=e)
await callback.answer('❌ Ошибка при сохранении', show_alert=True)
diff --git a/app/handlers/admin/reports.py b/app/handlers/admin/reports.py
index 50761ffa..0ec6cc04 100644
--- a/app/handlers/admin/reports.py
+++ b/app/handlers/admin/reports.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -18,7 +17,7 @@ from app.services.reporting_service import (
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -74,11 +73,11 @@ async def _send_report(
try:
report_text = await reporting_service.send_report(period, send_to_topic=True)
except ReportingServiceError as exc:
- logger.warning('Не удалось отправить отчет: %s', exc)
+ logger.warning('Не удалось отправить отчет', exc=exc)
await callback.answer(str(exc), show_alert=True)
return
except Exception as exc:
- logger.error('Непредвиденная ошибка при отправке отчета: %s', exc)
+ logger.error('Непредвиденная ошибка при отправке отчета', exc=exc)
await callback.answer('Не удалось отправить отчет. Попробуйте позже.', show_alert=True)
return
@@ -101,7 +100,7 @@ async def close_report_message(
try:
await callback.message.delete()
except (TelegramBadRequest, TelegramForbiddenError) as exc:
- logger.warning('Не удалось закрыть сообщение отчета: %s', exc)
+ logger.warning('Не удалось закрыть сообщение отчета', exc=exc)
await callback.answer(texts.t('REPORT_CLOSE_ERROR', 'Не удалось закрыть отчет.'), show_alert=True)
return
diff --git a/app/handlers/admin/rules.py b/app/handlers/admin/rules.py
index 36aef825..84ebe2d3 100644
--- a/app/handlers/admin/rules.py
+++ b/app/handlers/admin/rules.py
@@ -1,6 +1,6 @@
-import logging
import re
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ def _safe_preview(html_text: str, limit: int = 500) -> str:
return plain[:limit] + '...'
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -69,7 +69,7 @@ async def view_current_rules(callback: types.CallbackQuery, db_user: User, db: A
)
await callback.answer()
except Exception as e:
- logger.error(f'Ошибка при показе правил: {e}')
+ logger.error('Ошибка при показе правил', error=e)
await callback.message.edit_text(
'❌ Ошибка при загрузке правил. Возможно, в тексте есть некорректные HTML теги.',
reply_markup=types.InlineKeyboardMarkup(
@@ -112,7 +112,7 @@ async def start_edit_rules(callback: types.CallbackQuery, db_user: User, state:
await callback.answer()
except Exception as e:
- logger.error(f'Ошибка при начале редактирования правил: {e}')
+ logger.error('Ошибка при начале редактирования правил', error=e)
await callback.answer('❌ Ошибка при загрузке правил для редактирования', show_alert=True)
@@ -169,7 +169,7 @@ async def process_rules_edit(message: types.Message, db_user: User, state: FSMCo
await state.update_data(new_rules=new_rules)
except Exception as e:
- logger.error(f'Ошибка при показе превью правил: {e}')
+ logger.error('Ошибка при показе превью правил', error=e)
await message.answer(
'⚠️ Подтверждение сохранения правил\n\n'
f'Новые правила готовы к сохранению ({len(new_rules)} символов).\n'
@@ -240,11 +240,11 @@ async def save_rules(callback: types.CallbackQuery, db_user: User, state: FSMCon
)
await state.clear()
- logger.info(f'Правила сервиса обновлены администратором {db_user.telegram_id}')
+ logger.info('Правила сервиса обновлены администратором', telegram_id=db_user.telegram_id)
await callback.answer()
except Exception as e:
- logger.error(f'Ошибка сохранения правил: {e}')
+ logger.error('Ошибка сохранения правил', error=e)
await callback.message.edit_text(
'❌ Ошибка при сохранении правил\n\nПроизошла ошибка при записи в базу данных. Попробуйте еще раз.',
reply_markup=types.InlineKeyboardMarkup(
@@ -302,11 +302,11 @@ async def confirm_clear_rules(callback: types.CallbackQuery, db_user: User, db:
),
)
- logger.info(f'Правила очищены администратором {db_user.telegram_id}')
+ logger.info('Правила очищены администратором', telegram_id=db_user.telegram_id)
await callback.answer()
except Exception as e:
- logger.error(f'Ошибка при очистке правил: {e}')
+ logger.error('Ошибка при очистке правил', error=e)
await callback.answer('❌ Ошибка при очистке правил', show_alert=True)
diff --git a/app/handlers/admin/servers.py b/app/handlers/admin/servers.py
index 67ceb231..148b4e0d 100644
--- a/app/handlers/admin/servers.py
+++ b/app/handlers/admin/servers.py
@@ -1,6 +1,6 @@
import html
-import logging
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -24,7 +24,7 @@ from app.utils.cache import cache
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _build_server_edit_view(server):
@@ -262,7 +262,7 @@ async def sync_servers_with_remnawave(callback: types.CallbackQuery, db_user: Us
await callback.message.edit_text(text, reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard))
except Exception as e:
- logger.error(f'Ошибка синхронизации серверов: {e}')
+ logger.error('Ошибка синхронизации серверов', error=e)
await callback.message.edit_text(
f'❌ Ошибка синхронизации: {e!s}',
reply_markup=types.InlineKeyboardMarkup(
@@ -1092,7 +1092,7 @@ async def sync_server_user_counts_handler(callback: types.CallbackQuery, db_user
await callback.message.edit_text(text, reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard))
except Exception as e:
- logger.error(f'Ошибка синхронизации счетчиков: {e}')
+ logger.error('Ошибка синхронизации счетчиков', error=e)
await callback.message.edit_text(
f'❌ Ошибка синхронизации: {e!s}',
reply_markup=types.InlineKeyboardMarkup(
diff --git a/app/handlers/admin/statistics.py b/app/handlers/admin/statistics.py
index 906ee8e2..4d61c167 100644
--- a/app/handlers/admin/statistics.py
+++ b/app/handlers/admin/statistics.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime, timedelta
+import structlog
from aiogram import Dispatcher, F, types
from sqlalchemy.ext.asyncio import AsyncSession
@@ -15,7 +15,7 @@ from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_percentage
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -75,7 +75,7 @@ async def show_users_statistics(callback: types.CallbackQuery, db_user: User, db
if 'message is not modified' in str(e):
await callback.answer('📊 Данные актуальны', show_alert=False)
else:
- logger.error(f'Ошибка обновления статистики пользователей: {e}')
+ logger.error('Ошибка обновления статистики пользователей', error=e)
await callback.answer('❌ Ошибка обновления данных', show_alert=True)
return
@@ -126,7 +126,7 @@ async def show_subscriptions_statistics(callback: types.CallbackQuery, db_user:
if 'message is not modified' in str(e):
await callback.answer('📊 Данные актуальны', show_alert=False)
else:
- logger.error(f'Ошибка обновления статистики подписок: {e}')
+ logger.error('Ошибка обновления статистики подписок', error=e)
await callback.answer('❌ Ошибка обновления данных', show_alert=True)
@@ -181,7 +181,7 @@ async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User,
if 'message is not modified' in str(e):
await callback.answer('📊 Данные актуальны', show_alert=False)
else:
- logger.error(f'Ошибка обновления статистики доходов: {e}')
+ logger.error('Ошибка обновления статистики доходов', error=e)
await callback.answer('❌ Ошибка обновления данных', show_alert=True)
@@ -239,7 +239,7 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User,
if 'message is not modified' in str(e):
await callback.answer('📊 Данные актуальны', show_alert=False)
else:
- logger.error(f'Ошибка обновления реферальной статистики: {e}')
+ logger.error('Ошибка обновления реферальной статистики', error=e)
await callback.answer('❌ Ошибка обновления данных', show_alert=True)
@@ -302,7 +302,7 @@ async def show_summary_statistics(callback: types.CallbackQuery, db_user: User,
if 'message is not modified' in str(e):
await callback.answer('📊 Данные актуальны', show_alert=False)
else:
- logger.error(f'Ошибка обновления общей статистики: {e}')
+ logger.error('Ошибка обновления общей статистики', error=e)
await callback.answer('❌ Ошибка обновления данных', show_alert=True)
diff --git a/app/handlers/admin/subscriptions.py b/app/handlers/admin/subscriptions.py
index b88692f2..892e8181 100644
--- a/app/handlers/admin/subscriptions.py
+++ b/app/handlers/admin/subscriptions.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -58,11 +57,11 @@ async def get_users_by_countries(db: AsyncSession) -> dict:
return stats
except Exception as e:
- logger.error(f'Ошибка получения статистики по странам: {e}')
+ logger.error('Ошибка получения статистики по странам', error=e)
return {}
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
@@ -319,7 +318,7 @@ async def show_countries_management(callback: types.CallbackQuery, db_user: User
text += f'{country_flag} {country}: {count} пользователей\n'
except Exception as e:
- logger.error(f'Ошибка получения данных о странах: {e}')
+ logger.error('Ошибка получения данных о странах', error=e)
text = f"""
🌍 Управление странами
@@ -360,7 +359,7 @@ async def send_expiry_reminders(callback: types.CallbackQuery, db_user: User, db
user = subscription.user
# Skip email-only users (no telegram_id)
if not user.telegram_id:
- logger.debug(f'Пропуск email-пользователя {user.id} при отправке напоминания')
+ logger.debug('Пропуск email-пользователя при отправке напоминания', user_id=user.id)
continue
days_left = max(1, subscription.days_left)
@@ -379,7 +378,7 @@ async def send_expiry_reminders(callback: types.CallbackQuery, db_user: User, db
sent_count += 1
except Exception as e:
- logger.error(f'Ошибка отправки напоминания пользователю {subscription.user_id}: {e}')
+ logger.error('Ошибка отправки напоминания пользователю', user_id=subscription.user_id, error=e)
await callback.message.edit_text(
f'✅ Напоминания отправлены: {sent_count} из {len(expiring_subs)}',
diff --git a/app/handlers/admin/support_settings.py b/app/handlers/admin/support_settings.py
index e8d3cd56..38ab2407 100644
--- a/app/handlers/admin/support_settings.py
+++ b/app/handlers/admin/support_settings.py
@@ -1,8 +1,8 @@
import contextlib
import html
-import logging
import re
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
@@ -16,7 +16,7 @@ from app.states import SupportSettingsStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _get_support_settings_keyboard(language: str) -> types.InlineKeyboardMarkup:
diff --git a/app/handlers/admin/system_logs.py b/app/handlers/admin/system_logs.py
index 305c141a..0eb24ce1 100644
--- a/app/handlers/admin/system_logs.py
+++ b/app/handlers/admin/system_logs.py
@@ -1,8 +1,8 @@
-import logging
from datetime import datetime
from html import escape
from pathlib import Path
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.types import FSInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,7 +12,7 @@ from app.database.models import User
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
LOG_PREVIEW_LIMIT = 2300
@@ -41,7 +41,7 @@ def _build_logs_message(log_path: Path) -> str:
try:
content = log_path.read_text(encoding='utf-8', errors='ignore')
except Exception as error: # pragma: no cover - защита от проблем чтения
- logger.error('Ошибка чтения лог-файла %s: %s', log_path, error)
+ logger.error('Ошибка чтения лог-файла', log_path=log_path, error=error)
message = f'❌ Ошибка чтения логов\n\nНе удалось прочитать файл {log_path}.'
return message
@@ -134,7 +134,7 @@ async def download_system_logs(
)
await callback.message.answer_document(document=document, caption=caption, parse_mode='HTML')
except Exception as error: # pragma: no cover - защита от ошибок отправки
- logger.error('Ошибка отправки лог-файла %s: %s', log_path, error)
+ logger.error('Ошибка отправки лог-файла', log_path=log_path, error=error)
await callback.message.answer(
'❌ Не удалось отправить лог-файл\n\nПроверьте журналы приложения или повторите попытку позже.',
parse_mode='HTML',
diff --git a/app/handlers/admin/tariffs.py b/app/handlers/admin/tariffs.py
index 7c23dd5f..4e4db9d2 100644
--- a/app/handlers/admin/tariffs.py
+++ b/app/handlers/admin/tariffs.py
@@ -1,7 +1,6 @@
"""Управление тарифами в админ-панели."""
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -25,7 +24,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
ITEMS_PER_PAGE = 10
diff --git a/app/handlers/admin/tickets.py b/app/handlers/admin/tickets.py
index b5fea0e3..03c99aa1 100644
--- a/app/handlers/admin/tickets.py
+++ b/app/handlers/admin/tickets.py
@@ -1,8 +1,8 @@
import html
-import logging
import time
from datetime import datetime, timedelta
+import structlog
from aiogram import Bot, Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -22,7 +22,7 @@ from app.states import AdminTicketStates
from app.utils.cache import RateLimitCache
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Максимальная длина сообщения Telegram (с запасом)
MAX_MESSAGE_LEN = 3500
@@ -506,7 +506,7 @@ async def handle_admin_ticket_reply(message: types.Message, state: FSMContext, d
# Админ-уведомления о ответе в тикет отключены по требованию
except Exception as e:
- logger.error(f'Error adding admin ticket reply: {e}')
+ logger.error('Error adding admin ticket reply', error=e)
texts = get_texts(db_user.language)
await message.answer(
texts.t('TICKET_REPLY_ERROR', '❌ Произошла ошибка при отправке ответа. Попробуйте позже.')
@@ -533,7 +533,7 @@ async def mark_ticket_as_answered(callback: types.CallbackQuery, db_user: User,
await callback.answer(texts.t('TICKET_UPDATE_ERROR', '❌ Ошибка при обновлении тикета.'), show_alert=True)
except Exception as e:
- logger.error(f'Error marking ticket as answered: {e}')
+ logger.error('Error marking ticket as answered', error=e)
texts = get_texts(db_user.language)
await callback.answer(texts.t('TICKET_UPDATE_ERROR', '❌ Ошибка при обновлении тикета.'), show_alert=True)
@@ -550,7 +550,7 @@ async def close_all_open_admin_tickets(callback: types.CallbackQuery, db_user: U
try:
closed_ticket_ids = await TicketCRUD.close_all_open_tickets(db)
except Exception as error:
- logger.error('Error closing all open tickets: %s', error)
+ logger.error('Error closing all open tickets', error=error)
await callback.answer(texts.t('TICKET_UPDATE_ERROR', '❌ Ошибка при обновлении тикета.'), show_alert=True)
return
@@ -580,7 +580,7 @@ async def close_all_open_admin_tickets(callback: types.CallbackQuery, db_user: U
},
)
except Exception as audit_error:
- logger.warning('Failed to add support audit for bulk close: %s', audit_error)
+ logger.warning('Failed to add support audit for bulk close', audit_error=audit_error)
# Обновляем список тикетов
await show_admin_tickets(callback, db_user, db)
@@ -668,7 +668,7 @@ async def close_admin_ticket(callback: types.CallbackQuery, db_user: User, db: A
await callback.answer(texts.t('TICKET_CLOSE_ERROR', '❌ Ошибка при закрытии тикета.'), show_alert=True)
except Exception as e:
- logger.error(f'Error closing admin ticket: {e}')
+ logger.error('Error closing admin ticket', error=e)
texts = get_texts(db_user.language)
await callback.answer(texts.t('TICKET_CLOSE_ERROR', '❌ Ошибка при закрытии тикета.'), show_alert=True)
@@ -911,7 +911,7 @@ async def handle_admin_block_duration_input(message: types.Message, state: FSMCo
finally:
await state.clear()
except Exception as e:
- logger.error(f'Error setting block duration: {e}')
+ logger.error('Error setting block duration', error=e)
texts = get_texts(db_user.language)
await message.answer(texts.t('TICKET_REPLY_ERROR', '❌ Произошла ошибка. Попробуйте позже.'))
@@ -1041,15 +1041,15 @@ async def notify_user_about_ticket_reply(bot: Bot, ticket: Ticket, reply_text: s
user = getattr(ticket_with_user, 'user', None)
if not user:
- logger.error(f'User not found for ticket #{ticket.id}')
+ logger.error('User not found for ticket #', ticket_id=ticket.id)
return
if not getattr(user, 'telegram_id', None):
logger.warning(
- 'Cannot notify ticket #%s user without telegram_id (username=%s, auth_type=%s)',
- ticket.id,
- getattr(user, 'username', None),
- getattr(user, 'auth_type', None),
+ 'Cannot notify ticket # user without telegram_id (username auth_type=)',
+ ticket_id=ticket.id,
+ getattr=getattr(user, 'username', None),
+ getattr_2=getattr(user, 'auth_type', None),
)
return
@@ -1096,13 +1096,13 @@ async def notify_user_about_ticket_reply(bot: Bot, ticket: Ticket, reply_text: s
return
except TelegramBadRequest as photo_error:
logger.error(
- 'Не удалось отправить фото-уведомление пользователю %s для тикета %s: %s',
- chat_id,
- ticket.id,
- photo_error,
+ 'Не удалось отправить фото-уведомление пользователю для тикета',
+ chat_id=chat_id,
+ ticket_id=ticket.id,
+ photo_error=photo_error,
)
except Exception as e:
- logger.error(f'Не удалось отправить фото-уведомление: {e}')
+ logger.error('Не удалось отправить фото-уведомление', error=e)
# Фоллбек: текстовое уведомление
await bot.send_message(
chat_id=chat_id,
@@ -1110,10 +1110,10 @@ async def notify_user_about_ticket_reply(bot: Bot, ticket: Ticket, reply_text: s
reply_markup=keyboard,
)
- logger.info(f'Ticket #{ticket.id} reply notification sent to user {chat_id}')
+ logger.info('Ticket # reply notification sent to user', ticket_id=ticket.id, chat_id=chat_id)
except Exception as e:
- logger.error(f'Error notifying user about ticket reply: {e}')
+ logger.error('Error notifying user about ticket reply', error=e)
def register_handlers(dp: Dispatcher):
diff --git a/app/handlers/admin/trials.py b/app/handlers/admin/trials.py
index ea5278a9..5ef8378b 100644
--- a/app/handlers/admin/trials.py
+++ b/app/handlers/admin/trials.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +12,7 @@ from app.localization.texts import get_texts
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@admin_required
diff --git a/app/handlers/admin/updates.py b/app/handlers/admin/updates.py
index 62637e1b..cf2fd0e1 100644
--- a/app/handlers/admin/updates.py
+++ b/app/handlers/admin/updates.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy.ext.asyncio import AsyncSession
@@ -9,7 +8,7 @@ from app.services.version_service import version_service
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def get_updates_keyboard(language: str = 'ru') -> InlineKeyboardMarkup:
@@ -73,7 +72,7 @@ async def show_updates_menu(callback: types.CallbackQuery, db_user: User, db: As
logger.debug('📝 Сообщение не изменено в show_updates_menu')
await callback.answer()
return
- logger.error(f'Ошибка показа меню обновлений: {e}')
+ logger.error('Ошибка показа меню обновлений', error=e)
await callback.answer('❌ Ошибка загрузки меню обновлений', show_alert=True)
@@ -125,7 +124,7 @@ async def check_updates(callback: types.CallbackQuery, db_user: User, db: AsyncS
if 'message is not modified' in str(e).lower():
logger.debug('📝 Сообщение не изменено в check_updates')
return
- logger.error(f'Ошибка проверки обновлений: {e}')
+ logger.error('Ошибка проверки обновлений', error=e)
await callback.message.edit_text(
f'❌ ОШИБКА ПРОВЕРКИ ОБНОВЛЕНИЙ\n\n'
f'Не удалось связаться с сервером GitHub.\n'
@@ -207,7 +206,7 @@ async def show_version_info(callback: types.CallbackQuery, db_user: User, db: As
if 'message is not modified' in str(e).lower():
logger.debug('📝 Сообщение не изменено в show_version_info')
return
- logger.error(f'Ошибка получения информации о версиях: {e}')
+ logger.error('Ошибка получения информации о версиях', error=e)
await callback.message.edit_text(
f'❌ ОШИБКА ЗАГРУЗКИ\n\n'
f'Не удалось получить информацию о версиях.\n\n'
diff --git a/app/handlers/admin/user_messages.py b/app/handlers/admin/user_messages.py
index 6fb89e39..7ae5f4cc 100644
--- a/app/handlers/admin/user_messages.py
+++ b/app/handlers/admin/user_messages.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
@@ -25,7 +24,7 @@ from app.utils.validators import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class UserMessageStates(StatesGroup):
@@ -140,7 +139,7 @@ async def process_new_message_text(message: types.Message, state: FSMContext, db
)
except Exception as e:
- logger.error(f'Ошибка создания сообщения: {e}')
+ logger.error('Ошибка создания сообщения', error=e)
await state.clear()
await message.answer(
'❌ Произошла ошибка при создании сообщения. Попробуйте еще раз.',
@@ -405,7 +404,7 @@ async def process_edit_message_text(message: types.Message, state: FSMContext, d
)
except Exception as e:
- logger.error(f'Ошибка обновления сообщения: {e}')
+ logger.error('Ошибка обновления сообщения', error=e)
await state.clear()
await message.answer(
'❌ Произошла ошибка при обновлении сообщения.', reply_markup=get_user_messages_keyboard(db_user.language)
diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py
index cfc29f0d..ead1e744 100644
--- a/app/handlers/admin/users.py
+++ b/app/handlers/admin/users.py
@@ -1,10 +1,10 @@
-import logging
import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from typing import Any
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.fsm.context import FSMContext
@@ -53,7 +53,7 @@ from app.utils.subscription_utils import (
from app.utils.user_utils import get_effective_referral_commission_percent
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# =============================================================================
@@ -645,7 +645,7 @@ async def handle_users_list_pagination_fixed(
page = int(callback_parts[-1])
await show_users_list(callback, db_user, db, state, page)
except (ValueError, IndexError) as e:
- logger.error(f'Ошибка парсинга номера страницы: {e}')
+ logger.error('Ошибка парсинга номера страницы', error=e)
await show_users_list(callback, db_user, db, state, 1)
@@ -659,7 +659,7 @@ async def handle_users_balance_list_pagination(
page = int(callback_parts[-1])
await show_users_list_by_balance(callback, db_user, db, state, page)
except (ValueError, IndexError) as e:
- logger.error(f'Ошибка парсинга номера страницы: {e}')
+ logger.error('Ошибка парсинга номера страницы', error=e)
await show_users_list_by_balance(callback, db_user, db, state, 1)
@@ -672,7 +672,7 @@ async def handle_users_ready_to_renew_pagination(
page = int(callback.data.split('_')[-1])
await show_users_ready_to_renew(callback, db_user, db, state, page)
except (ValueError, IndexError) as e:
- logger.error(f'Ошибка парсинга номера страницы: {e}')
+ logger.error('Ошибка парсинга номера страницы', error=e)
await show_users_ready_to_renew(callback, db_user, db, state, 1)
@@ -685,7 +685,7 @@ async def handle_potential_customers_pagination(
page = int(callback.data.split('_')[-1])
await show_potential_customers(callback, db_user, db, state, page)
except (ValueError, IndexError) as e:
- logger.error(f'Ошибка парсинга номера страницы: {e}')
+ logger.error('Ошибка парсинга номера страницы', error=e)
await show_potential_customers(callback, db_user, db, state, 1)
@@ -699,7 +699,7 @@ async def handle_users_campaign_list_pagination(
page = int(callback_parts[-1])
await show_users_list_by_campaign(callback, db_user, db, state, page)
except (ValueError, IndexError) as e:
- logger.error(f'Ошибка парсинга номера страницы: {e}')
+ logger.error('Ошибка парсинга номера страницы', error=e)
await show_users_list_by_campaign(callback, db_user, db, state, 1)
@@ -873,7 +873,7 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db:
else:
text += f'• {squad_uuid[:8]}... (неизвестный)\n'
except Exception as e:
- logger.error(f'Ошибка получения сервера {squad_uuid}: {e}')
+ logger.error('Ошибка получения сервера', squad_uuid=squad_uuid, error=e)
text += f'• {squad_uuid[:8]}... (ошибка загрузки)\n'
else:
text += '\nПодключенные серверы: отсутствуют\n'
@@ -1565,23 +1565,16 @@ async def _update_referral_commission_percent(
effective = get_effective_referral_commission_percent(user)
logger.info(
- 'Админ %s обновил реферальный процент пользователя %s: %s',
- admin_id,
- user_id,
- percent,
+ 'Админ обновил реферальный процент пользователя', admin_id=admin_id, user_id=user_id, percent=percent
)
return True, effective
except Exception as e:
- logger.error(
- 'Ошибка обновления реферального процента пользователя %s: %s',
- user_id,
- e,
- )
+ logger.error('Ошибка обновления реферального процента пользователя', user_id=user_id, e=e)
try:
await db.rollback()
except Exception as rollback_error:
- logger.error('Ошибка отката транзакции: %s', rollback_error)
+ logger.error('Ошибка отката транзакции', rollback_error=rollback_error)
return False, None
@@ -2249,7 +2242,7 @@ async def process_send_user_message(
reply_markup=confirmation_keyboard,
)
except TelegramBadRequest as err:
- logger.error('Ошибка отправки сообщения пользователю %s: %s', target_user.telegram_id, err)
+ logger.error('Ошибка отправки сообщения пользователю', telegram_id=target_user.telegram_id, err=err)
await message.answer(
texts.t(
'ADMIN_USER_SEND_MESSAGE_BAD_REQUEST',
@@ -2260,7 +2253,7 @@ async def process_send_user_message(
await state.clear()
return
except Exception as err:
- logger.error('Неожиданная ошибка отправки сообщения пользователю %s: %s', target_user.telegram_id, err)
+ logger.error('Неожиданная ошибка отправки сообщения пользователю', telegram_id=target_user.telegram_id, err=err)
await message.answer(
texts.t('ADMIN_USER_SEND_MESSAGE_ERROR', '❌ Не удалось отправить сообщение. Попробуйте позже.'),
reply_markup=confirmation_keyboard,
@@ -3444,7 +3437,7 @@ async def _show_servers_for_user(callback: types.CallbackQuery, user_id: int, db
await callback.message.edit_text(text, reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard))
except Exception as e:
- logger.error(f'Ошибка показа серверов: {e}')
+ logger.error('Ошибка показа серверов', error=e)
@admin_required
@@ -3491,16 +3484,22 @@ async def toggle_user_server(callback: types.CallbackQuery, db_user: User, db: A
full_name=user.full_name, username=user.username, telegram_id=user.telegram_id
),
)
- logger.info(f'✅ Обновлены серверы в RemnaWave для пользователя {user.telegram_id}')
+ logger.info('✅ Обновлены серверы в RemnaWave для пользователя', telegram_id=user.telegram_id)
except Exception as rw_error:
- logger.error(f'❌ Ошибка обновления RemnaWave: {rw_error}')
+ logger.error('❌ Ошибка обновления RemnaWave', rw_error=rw_error)
- logger.info(f'Админ {db_user.id}: сервер {server.display_name} {action_text} для пользователя {user_id}')
+ logger.info(
+ 'Админ сервер для пользователя',
+ db_user_id=db_user.id,
+ display_name=server.display_name,
+ action_text=action_text,
+ user_id=user_id,
+ )
await refresh_server_selection_screen(callback, user_id, db_user, db)
except Exception as e:
- logger.error(f'Ошибка переключения сервера: {e}')
+ logger.error('Ошибка переключения сервера', error=e)
await callback.answer('❌ Ошибка изменения сервера', show_alert=True)
@@ -3554,7 +3553,7 @@ async def refresh_server_selection_screen(callback: types.CallbackQuery, user_id
await callback.message.edit_text(text, reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard))
except Exception as e:
- logger.error(f'Ошибка обновления экрана серверов: {e}')
+ logger.error('Ошибка обновления экрана серверов', error=e)
@admin_required
@@ -3628,7 +3627,9 @@ async def set_user_devices_button(callback: types.CallbackQuery, db_user: User,
await callback.answer()
- logger.info(f'Админ {db_user.telegram_id} {action_text} модем для пользователя {user_id}')
+ logger.info(
+ 'Админ модем для пользователя', telegram_id=db_user.telegram_id, action_text=action_text, user_id=user_id
+ )
await callback.answer()
@@ -3842,7 +3843,7 @@ async def reset_user_devices(callback: types.CallbackQuery, db_user: User, db: A
]
),
)
- logger.info(f'Админ {db_user.id} сбросил устройства пользователя {user_id}')
+ logger.info('Админ сбросил устройства пользователя', db_user_id=db_user.id, user_id=user_id)
else:
await callback.message.edit_text(
'❌ Ошибка сброса устройств',
@@ -3858,7 +3859,7 @@ async def reset_user_devices(callback: types.CallbackQuery, db_user: User, db: A
)
except Exception as e:
- logger.error(f'Ошибка сброса устройств: {e}')
+ logger.error('Ошибка сброса устройств', error=e)
await callback.answer('❌ Ошибка сброса устройств', show_alert=True)
@@ -3866,7 +3867,7 @@ async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, adm
try:
user = await get_user_by_id(db, user_id)
if not user or not user.subscription:
- logger.error(f'Пользователь {user_id} или подписка не найдены')
+ logger.error('Пользователь или подписка не найдены', user_id=user_id)
return False
subscription = user.subscription
@@ -3887,15 +3888,21 @@ async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, adm
full_name=user.full_name, username=user.username, telegram_id=user.telegram_id
),
)
- logger.info(f'✅ Обновлен лимит устройств в RemnaWave для пользователя {user.telegram_id}')
+ logger.info('✅ Обновлен лимит устройств в RemnaWave для пользователя', telegram_id=user.telegram_id)
except Exception as rw_error:
- logger.error(f'❌ Ошибка обновления лимита устройств в RemnaWave: {rw_error}')
+ logger.error('❌ Ошибка обновления лимита устройств в RemnaWave', rw_error=rw_error)
- logger.info(f'Админ {admin_id} изменил лимит устройств пользователя {user_id}: {old_devices} -> {devices}')
+ logger.info(
+ 'Админ изменил лимит устройств пользователя',
+ admin_id=admin_id,
+ user_id=user_id,
+ old_devices=old_devices,
+ devices=devices,
+ )
return True
except Exception as e:
- logger.error(f'Ошибка обновления лимита устройств: {e}')
+ logger.error('Ошибка обновления лимита устройств', error=e)
await db.rollback()
return False
@@ -3904,7 +3911,7 @@ async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int,
try:
user = await get_user_by_id(db, user_id)
if not user or not user.subscription:
- logger.error(f'Пользователь {user_id} или подписка не найдены')
+ logger.error('Пользователь или подписка не найдены', user_id=user_id)
return False
subscription = user.subscription
@@ -3928,19 +3935,23 @@ async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int,
full_name=user.full_name, username=user.username, telegram_id=user.telegram_id
),
)
- logger.info(f'✅ Обновлен лимит трафика в RemnaWave для пользователя {user.telegram_id}')
+ logger.info('✅ Обновлен лимит трафика в RemnaWave для пользователя', telegram_id=user.telegram_id)
except Exception as rw_error:
- logger.error(f'❌ Ошибка обновления лимита трафика в RemnaWave: {rw_error}')
+ logger.error('❌ Ошибка обновления лимита трафика в RemnaWave', rw_error=rw_error)
traffic_text_old = 'безлимитный' if old_traffic == 0 else f'{old_traffic} ГБ'
traffic_text_new = 'безлимитный' if traffic_gb == 0 else f'{traffic_gb} ГБ'
logger.info(
- f'Админ {admin_id} изменил лимит трафика пользователя {user_id}: {traffic_text_old} -> {traffic_text_new}'
+ 'Админ изменил лимит трафика пользователя',
+ admin_id=admin_id,
+ user_id=user_id,
+ traffic_text_old=traffic_text_old,
+ traffic_text_new=traffic_text_new,
)
return True
except Exception as e:
- logger.error(f'Ошибка обновления лимита трафика: {e}')
+ logger.error('Ошибка обновления лимита трафика', error=e)
await db.rollback()
return False
@@ -3952,7 +3963,7 @@ async def _extend_subscription_by_days(db: AsyncSession, user_id: int, days: int
subscription = await get_subscription_by_user_id(db, user_id)
if not subscription:
- logger.error(f'Подписка не найдена для пользователя {user_id}')
+ logger.error('Подписка не найдена для пользователя', user_id=user_id)
return False
await extend_subscription(db, subscription, days)
@@ -3961,13 +3972,15 @@ async def _extend_subscription_by_days(db: AsyncSession, user_id: int, days: int
await subscription_service.update_remnawave_user(db, subscription)
if days > 0:
- logger.info(f'Админ {admin_id} продлил подписку пользователя {user_id} на {days} дней')
+ logger.info('Админ продлил подписку пользователя на дней', admin_id=admin_id, user_id=user_id, days=days)
else:
- logger.info(f'Админ {admin_id} сократил подписку пользователя {user_id} на {abs(days)} дней')
+ logger.info(
+ 'Админ сократил подписку пользователя на дней', admin_id=admin_id, user_id=user_id, value=abs(days)
+ )
return True
except Exception as e:
- logger.error(f'Ошибка продления подписки: {e}')
+ logger.error('Ошибка продления подписки', error=e)
return False
@@ -3978,7 +3991,7 @@ async def _add_subscription_traffic(db: AsyncSession, user_id: int, gb: int, adm
subscription = await get_subscription_by_user_id(db, user_id)
if not subscription:
- logger.error(f'Подписка не найдена для пользователя {user_id}')
+ logger.error('Подписка не найдена для пользователя', user_id=user_id)
return False
if gb == 0:
@@ -3991,11 +4004,11 @@ async def _add_subscription_traffic(db: AsyncSession, user_id: int, gb: int, adm
await subscription_service.update_remnawave_user(db, subscription)
traffic_text = 'безлимитный' if gb == 0 else f'{gb} ГБ'
- logger.info(f'Админ {admin_id} добавил трафик {traffic_text} пользователю {user_id}')
+ logger.info('Админ добавил трафик пользователю', admin_id=admin_id, traffic_text=traffic_text, user_id=user_id)
return True
except Exception as e:
- logger.error(f'Ошибка добавления трафика: {e}')
+ logger.error('Ошибка добавления трафика', error=e)
return False
@@ -4006,7 +4019,7 @@ async def _deactivate_user_subscription(db: AsyncSession, user_id: int, admin_id
subscription = await get_subscription_by_user_id(db, user_id)
if not subscription:
- logger.error(f'Подписка не найдена для пользователя {user_id}')
+ logger.error('Подписка не найдена для пользователя', user_id=user_id)
return False
await deactivate_subscription(db, subscription)
@@ -4016,11 +4029,11 @@ async def _deactivate_user_subscription(db: AsyncSession, user_id: int, admin_id
subscription_service = SubscriptionService()
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
- logger.info(f'Админ {admin_id} деактивировал подписку пользователя {user_id}')
+ logger.info('Админ деактивировал подписку пользователя', admin_id=admin_id, user_id=user_id)
return True
except Exception as e:
- logger.error(f'Ошибка деактивации подписки: {e}')
+ logger.error('Ошибка деактивации подписки', error=e)
return False
@@ -4034,7 +4047,7 @@ async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id:
subscription = await get_subscription_by_user_id(db, user_id)
if not subscription:
- logger.error(f'Подписка не найдена для пользователя {user_id}')
+ logger.error('Подписка не найдена для пользователя', user_id=user_id)
return False
subscription.status = SubscriptionStatus.ACTIVE.value
@@ -4047,11 +4060,11 @@ async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id:
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
- logger.info(f'Админ {admin_id} активировал подписку пользователя {user_id}')
+ logger.info('Админ активировал подписку пользователя', admin_id=admin_id, user_id=user_id)
return True
except Exception as e:
- logger.error(f'Ошибка активации подписки: {e}')
+ logger.error('Ошибка активации подписки', error=e)
return False
@@ -4062,7 +4075,7 @@ async def _grant_trial_subscription(db: AsyncSession, user_id: int, admin_id: in
existing_subscription = await get_subscription_by_user_id(db, user_id)
if existing_subscription:
- logger.error(f'У пользователя {user_id} уже есть подписка')
+ logger.error('У пользователя уже есть подписка', user_id=user_id)
return False
forced_devices = None
@@ -4078,11 +4091,11 @@ async def _grant_trial_subscription(db: AsyncSession, user_id: int, admin_id: in
subscription_service = SubscriptionService()
await subscription_service.create_remnawave_user(db, subscription)
- logger.info(f'Админ {admin_id} выдал триальную подписку пользователю {user_id}')
+ logger.info('Админ выдал триальную подписку пользователю', admin_id=admin_id, user_id=user_id)
return True
except Exception as e:
- logger.error(f'Ошибка выдачи триальной подписки: {e}')
+ logger.error('Ошибка выдачи триальной подписки', error=e)
return False
@@ -4094,7 +4107,7 @@ async def _grant_paid_subscription(db: AsyncSession, user_id: int, days: int, ad
existing_subscription = await get_subscription_by_user_id(db, user_id)
if existing_subscription:
- logger.error(f'У пользователя {user_id} уже есть подписка')
+ logger.error('У пользователя уже есть подписка', user_id=user_id)
return False
trial_squads: list[str] = []
@@ -4106,11 +4119,7 @@ async def _grant_paid_subscription(db: AsyncSession, user_id: int, days: int, ad
if trial_uuid:
trial_squads = [trial_uuid]
except Exception as error:
- logger.error(
- 'Не удалось подобрать сквад при выдаче подписки админом %s: %s',
- admin_id,
- error,
- )
+ logger.error('Не удалось подобрать сквад при выдаче подписки админом', admin_id=admin_id, error=error)
forced_devices = None
if not settings.is_devices_selection_enabled():
@@ -4133,11 +4142,11 @@ async def _grant_paid_subscription(db: AsyncSession, user_id: int, days: int, ad
subscription_service = SubscriptionService()
await subscription_service.create_remnawave_user(db, subscription)
- logger.info(f'Админ {admin_id} выдал платную подписку на {days} дней пользователю {user_id}')
+ logger.info('Админ выдал платную подписку на дней пользователю', admin_id=admin_id, days=days, user_id=user_id)
return True
except Exception as e:
- logger.error(f'Ошибка выдачи платной подписки: {e}')
+ logger.error('Ошибка выдачи платной подписки', error=e)
return False
@@ -4160,14 +4169,14 @@ async def _calculate_subscription_period_price(
server_ids = await get_server_ids_by_uuids(db, connected_squads)
if len(server_ids) != len(connected_squads):
logger.warning(
- 'Не удалось сопоставить все сервера подписки пользователя %s для расчёта цены',
- target_user.telegram_id,
+ 'Не удалось сопоставить все сервера подписки пользователя для расчёта цены',
+ telegram_id=target_user.telegram_id,
)
except Exception as e:
logger.error(
- 'Не удалось получить идентификаторы серверов для расчёта цены подписки пользователя %s: %s',
- target_user.telegram_id,
- e,
+ 'Не удалось получить идентификаторы серверов для расчёта цены подписки пользователя',
+ telegram_id=target_user.telegram_id,
+ e=e,
)
server_ids = []
traffic_limit_gb = subscription.traffic_limit_gb
@@ -4282,10 +4291,10 @@ async def admin_buy_subscription(callback: types.CallbackQuery, db_user: User, d
)
except Exception as e:
logger.error(
- 'Ошибка расчёта стоимости подписки для пользователя %s и периода %s дней: %s',
- target_user.telegram_id,
- period,
- e,
+ 'Ошибка расчёта стоимости подписки для пользователя и периода дней',
+ telegram_id=target_user.telegram_id,
+ period=period,
+ e=e,
)
continue
@@ -4363,19 +4372,19 @@ async def admin_buy_subscription_confirm(callback: types.CallbackQuery, db_user:
)
except Exception as e:
logger.error(
- 'Ошибка расчёта стоимости подписки при подтверждении админом для пользователя %s: %s',
- target_user.telegram_id,
- e,
+ 'Ошибка расчёта стоимости подписки при подтверждении админом для пользователя',
+ telegram_id=target_user.telegram_id,
+ e=e,
)
await callback.answer('❌ Не удалось рассчитать стоимость подписки', show_alert=True)
return
if price_kopeks_from_callback is not None and price_kopeks_from_callback != price_kopeks:
logger.info(
- 'Стоимость подписки для пользователя %s изменилась с %s до %s при подтверждении',
- target_user.telegram_id,
- price_kopeks_from_callback,
- price_kopeks,
+ 'Стоимость подписки для пользователя изменилась с до при подтверждении',
+ telegram_id=target_user.telegram_id,
+ price_kopeks_from_callback=price_kopeks_from_callback,
+ price_kopeks=price_kopeks,
)
if target_user.balance_kopeks < price_kopeks:
@@ -4467,19 +4476,19 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
)
except Exception as e:
logger.error(
- 'Ошибка расчёта стоимости подписки при списании средств админом для пользователя %s: %s',
- target_user.telegram_id,
- e,
+ 'Ошибка расчёта стоимости подписки при списании средств админом для пользователя',
+ telegram_id=target_user.telegram_id,
+ e=e,
)
await callback.answer('❌ Не удалось рассчитать стоимость подписки', show_alert=True)
return
if price_kopeks_from_callback is not None and price_kopeks_from_callback != price_kopeks:
logger.info(
- 'Стоимость подписки для пользователя %s изменилась с %s до %s перед списанием',
- target_user.telegram_id,
- price_kopeks_from_callback,
- price_kopeks,
+ 'Стоимость подписки для пользователя изменилась с до перед списанием',
+ telegram_id=target_user.telegram_id,
+ price_kopeks_from_callback=price_kopeks_from_callback,
+ price_kopeks=price_kopeks,
)
if target_user.balance_kopeks < price_kopeks:
@@ -4506,9 +4515,9 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
if remaining_trial_delta.total_seconds() > 0:
bonus_period = remaining_trial_delta
logger.info(
- 'Админ продлевает подписку: добавляем оставшееся время триала (%s) пользователю %s',
- bonus_period,
- target_user.telegram_id,
+ 'Админ продлевает подписку: добавляем оставшееся время триала пользователю',
+ bonus_period=bonus_period,
+ telegram_id=target_user.telegram_id,
)
extension_base_date = current_time
@@ -4613,11 +4622,11 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
await db.commit()
if remnawave_user:
- logger.info(f'Пользователь {target_user.telegram_id} успешно обновлен в RemnaWave')
+ logger.info('Пользователь успешно обновлен в RemnaWave', telegram_id=target_user.telegram_id)
else:
- logger.error(f'Ошибка обновления пользователя {target_user.telegram_id} в RemnaWave')
+ logger.error('Ошибка обновления пользователя в RemnaWave', telegram_id=target_user.telegram_id)
except Exception as e:
- logger.error(f'Ошибка работы с RemnaWave для пользователя {target_user.telegram_id}: {e}')
+ logger.error('Ошибка работы с RemnaWave для пользователя', telegram_id=target_user.telegram_id, error=e)
message = f'✅ Подписка пользователя продлена на {period_days} дней'
else:
@@ -4658,12 +4667,12 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
)
except Exception as e:
user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
- logger.error(f'Ошибка отправки уведомления пользователю {user_id_display}: {e}')
+ logger.error('Ошибка отправки уведомления пользователю', user_id_display=user_id_display, error=e)
await callback.answer()
except Exception as e:
- logger.error(f'Ошибка покупки подписки администратором: {e}')
+ logger.error('Ошибка покупки подписки администратором', error=e)
await callback.answer('❌ Ошибка при покупке подписки', show_alert=True)
await db.rollback()
@@ -4977,7 +4986,7 @@ async def admin_buy_tariff_execute(callback: types.CallbackQuery, db_user: User,
reset_reason='покупка тарифа (администратор)',
)
except Exception as e:
- logger.error(f'Ошибка обновления Remnawave: {e}')
+ logger.error('Ошибка обновления Remnawave', error=e)
# Создаем транзакцию
await create_transaction(
@@ -5032,12 +5041,12 @@ async def admin_buy_tariff_execute(callback: types.CallbackQuery, db_user: User,
parse_mode='HTML',
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления пользователю: {e}')
+ logger.error('Ошибка отправки уведомления пользователю', error=e)
await callback.answer('✅ Тариф куплен!', show_alert=True)
except Exception as e:
- logger.error(f'Ошибка покупки тарифа администратором: {e}', exc_info=True)
+ logger.error('Ошибка покупки тарифа администратором', error=e, exc_info=True)
await callback.answer('❌ Ошибка при покупке тарифа', show_alert=True)
await db.rollback()
@@ -5081,13 +5090,13 @@ async def _change_subscription_type(db: AsyncSession, user_id: int, new_type: st
subscription = await get_subscription_by_user_id(db, user_id)
if not subscription:
- logger.error(f'Подписка не найдена для пользователя {user_id}')
+ logger.error('Подписка не найдена для пользователя', user_id=user_id)
return False
new_is_trial = new_type == 'trial'
if subscription.is_trial == new_is_trial:
- logger.info(f'Тип подписки уже установлен корректно для пользователя {user_id}')
+ logger.info('Тип подписки уже установлен корректно для пользователя', user_id=user_id)
return True
old_type = 'триальной' if subscription.is_trial else 'платной'
@@ -5106,11 +5115,17 @@ async def _change_subscription_type(db: AsyncSession, user_id: int, new_type: st
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
- logger.info(f'Админ {admin_id} изменил тип подписки пользователя {user_id}: {old_type} -> {new_type_text}')
+ logger.info(
+ 'Админ изменил тип подписки пользователя',
+ admin_id=admin_id,
+ user_id=user_id,
+ old_type=old_type,
+ new_type_text=new_type_text,
+ )
return True
except Exception as e:
- logger.error(f'Ошибка изменения типа подписки: {e}')
+ logger.error('Ошибка изменения типа подписки', error=e)
await db.rollback()
return False
@@ -5310,7 +5325,12 @@ async def confirm_admin_tariff_change(callback: types.CallbackQuery, db_user: Us
await subscription_service.update_remnawave_user(db, subscription)
logger.info(
- f'Админ {db_user.id} изменил тариф пользователя {user_id}: {old_tariff_id} -> {tariff_id} ({tariff.name})'
+ 'Админ изменил тариф пользователя',
+ db_user_id=db_user.id,
+ user_id=user_id,
+ old_tariff_id=old_tariff_id,
+ tariff_id=tariff_id,
+ tariff_name=tariff.name,
)
await callback.message.edit_text(
@@ -5331,7 +5351,7 @@ async def confirm_admin_tariff_change(callback: types.CallbackQuery, db_user: Us
)
except Exception as e:
- logger.error(f'Ошибка смены тарифа: {e}')
+ logger.error('Ошибка смены тарифа', error=e)
await db.rollback()
await callback.message.edit_text(
diff --git a/app/handlers/admin/welcome_text.py b/app/handlers/admin/welcome_text.py
index 2d371716..8c9c9f79 100644
--- a/app/handlers/admin/welcome_text.py
+++ b/app/handlers/admin/welcome_text.py
@@ -1,6 +1,6 @@
-import logging
import re
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -18,7 +18,7 @@ from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def validate_html_tags(text: str) -> tuple[bool, str]:
diff --git a/app/handlers/balance/cloudpayments.py b/app/handlers/balance/cloudpayments.py
index d49b06a1..e24c93cf 100644
--- a/app/handlers/balance/cloudpayments.py
+++ b/app/handlers/balance/cloudpayments.py
@@ -1,7 +1,6 @@
"""Handler for CloudPayments balance top-up."""
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
@@ -16,7 +15,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _create_cloudpayments_payment_and_respond(
@@ -118,11 +117,7 @@ async def _create_cloudpayments_payment_and_respond(
parse_mode='HTML',
)
- logger.info(
- 'CloudPayments payment created: user=%s, amount=%s₽',
- db_user.telegram_id,
- amount_rub,
- )
+ logger.info('CloudPayments payment created: user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub)
@error_handler
@@ -375,11 +370,7 @@ async def process_cloudpayments_amount(
parse_mode='HTML',
)
- logger.info(
- 'CloudPayments payment created: user=%s, amount=%s₽',
- db_user.telegram_id,
- amount_rub,
- )
+ logger.info('CloudPayments payment created: user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub)
@error_handler
@@ -498,7 +489,5 @@ async def handle_cloudpayments_quick_amount(
)
logger.info(
- 'CloudPayments payment created (quick): user=%s, amount=%s₽',
- db_user.telegram_id,
- amount_rub,
+ 'CloudPayments payment created (quick): user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub
)
diff --git a/app/handlers/balance/cryptobot.py b/app/handlers/balance/cryptobot.py
index 6e7b4dab..3d9208db 100644
--- a/app/handlers/balance/cryptobot.py
+++ b/app/handlers/balance/cryptobot.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +12,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -47,7 +46,7 @@ async def start_cryptobot_payment(callback: types.CallbackQuery, db_user: User,
current_rate = await currency_converter.get_usd_to_rub_rate()
rate_text = f'💱 Текущий курс: 1 USD = {current_rate:.2f} ₽'
except Exception as e:
- logger.warning(f'Не удалось получить курс валют: {e}')
+ logger.warning('Не удалось получить курс валют', error=e)
current_rate = 95.0
rate_text = f'💱 Курс: 1 USD ≈ {current_rate:.0f} ₽'
@@ -208,19 +207,13 @@ async def process_cryptobot_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning(
- 'Не удалось удалить сообщение с суммой CryptoBot: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с суммой CryptoBot', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - diagnostics
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы CryptoBot: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы CryptoBot', delete_error=delete_error)
invoice_message = await message.answer(
f'🪙 Оплата криптовалютой\n\n'
@@ -249,12 +242,15 @@ async def process_cryptobot_payment_amount(
await state.clear()
logger.info(
- f'Создан CryptoBot платеж для пользователя {db_user.telegram_id}: '
- f'{amount_rubles:.0f} ₽ ({amount_usd:.2f} USD), ID: {payment_result["invoice_id"]}'
+ 'Создан CryptoBot платеж для пользователя ₽ ( USD), ID',
+ telegram_id=db_user.telegram_id,
+ amount_rubles=round(amount_rubles, 0),
+ amount_usd=round(amount_usd, 2),
+ payment_result=payment_result['invoice_id'],
)
except Exception as e:
- logger.error(f'Ошибка создания CryptoBot платежа: {e}')
+ logger.error('Ошибка создания CryptoBot платежа', error=e)
await message.answer('❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.')
await state.clear()
@@ -297,5 +293,5 @@ async def check_cryptobot_payment_status(callback: types.CallbackQuery, db: Asyn
await callback.answer(message_text, show_alert=True)
except Exception as e:
- logger.error(f'Ошибка проверки статуса CryptoBot платежа: {e}')
+ logger.error('Ошибка проверки статуса CryptoBot платежа', error=e)
await callback.answer('❌ Ошибка проверки статуса', show_alert=True)
diff --git a/app/handlers/balance/freekassa.py b/app/handlers/balance/freekassa.py
index 5506f061..40180d0b 100644
--- a/app/handlers/balance/freekassa.py
+++ b/app/handlers/balance/freekassa.py
@@ -1,7 +1,6 @@
"""Handler for Freekassa balance top-up."""
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
@@ -16,7 +15,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _create_freekassa_payment_and_respond(
@@ -119,11 +118,7 @@ async def _create_freekassa_payment_and_respond(
parse_mode='HTML',
)
- logger.info(
- 'Freekassa payment created: user=%s, amount=%s₽',
- db_user.telegram_id,
- amount_rub,
- )
+ logger.info('Freekassa payment created: user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub)
@error_handler
diff --git a/app/handlers/balance/heleket.py b/app/handlers/balance/heleket.py
index 7cf5512f..d716580b 100644
--- a/app/handlers/balance/heleket.py
+++ b/app/handlers/balance/heleket.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy import update
@@ -15,7 +15,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -228,16 +228,13 @@ async def process_heleket_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning('Не удалось удалить сообщение с суммой Heleket: %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой Heleket', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы Heleket: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы Heleket', delete_error=delete_error)
invoice_message = await message.answer('\n'.join(details), parse_mode='HTML', reply_markup=keyboard)
@@ -258,7 +255,7 @@ async def process_heleket_payment_amount(
)
await db.commit()
except Exception as error: # pragma: no cover - diagnostics
- logger.warning('Не удалось сохранить сообщение Heleket: %s', error)
+ logger.warning('Не удалось сохранить сообщение Heleket', error=error)
await state.update_data(
heleket_invoice_message_id=invoice_message.message_id,
diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py
index c75b16ef..55c449af 100644
--- a/app/handlers/balance/kassa_ai.py
+++ b/app/handlers/balance/kassa_ai.py
@@ -1,7 +1,6 @@
"""Handler for KassaAI balance top-up."""
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
@@ -16,7 +15,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _create_kassa_ai_payment_and_respond(
@@ -119,11 +118,7 @@ async def _create_kassa_ai_payment_and_respond(
parse_mode='HTML',
)
- logger.info(
- 'KassaAI payment created: user=%s, amount=%s₽',
- db_user.telegram_id,
- amount_rub,
- )
+ logger.info('KassaAI payment created: user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub)
@error_handler
diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py
index 7cb6fc00..71e6e41a 100644
--- a/app/handlers/balance/main.py
+++ b/app/handlers/balance/main.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -21,7 +20,7 @@ from app.utils.decorators import error_handler
from app.utils.price_display import calculate_user_price
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
TRANSACTIONS_PER_PAGE = 10
@@ -230,10 +229,7 @@ async def show_balance_menu(callback: types.CallbackQuery, db_user: User, db: As
else:
await callback.message.answer(balance_text, reply_markup=reply_markup)
except TelegramBadRequest as error:
- logger.warning(
- 'Failed to edit balance message, sending a new one instead: %s',
- error,
- )
+ logger.warning('Failed to edit balance message, sending a new one instead', error=error)
await callback.message.answer(balance_text, reply_markup=reply_markup)
await callback.answer()
@@ -435,7 +431,9 @@ async def show_payment_methods(callback: types.CallbackQuery, db_user: User, db:
tariff_info = f'\n\n📋 Ваш текущий тариф:\n{current_tariff_desc}\n{estimated_price_info}'
except Exception as e:
- logger.warning(f'Не удалось рассчитать стоимость текущей подписки для пользователя {db_user.id}: {e}')
+ logger.warning(
+ 'Не удалось рассчитать стоимость текущей подписки для пользователя', db_user_id=db_user.id, error=e
+ )
tariff_info = ''
full_text = payment_text + tariff_info
@@ -489,7 +487,7 @@ async def handle_successful_topup_with_cart(user_id: int, amount_kopeks: int, bo
# Email-only users don't have telegram_id - skip Telegram notification
if not user.telegram_id:
- logger.info(f'Skipping cart notification for email-only user {user_id}')
+ logger.info('Skipping cart notification for email-only user', user_id=user_id)
return
storage = dp.storage
@@ -532,7 +530,7 @@ async def handle_successful_topup_with_cart(user_id: int, amount_kopeks: int, bo
)
except Exception as e:
- logger.error(f'Ошибка обработки успешного пополнения с корзиной: {e}')
+ logger.error('Ошибка обработки успешного пополнения с корзиной', error=e)
@error_handler
@@ -674,7 +672,7 @@ async def handle_sbp_payment(callback: types.CallbackQuery, db: AsyncSession):
await callback.answer('Информация об оплате отправлена', show_alert=True)
except Exception as e:
- logger.error(f'Ошибка обработки embedded платежа СБП: {e}')
+ logger.error('Ошибка обработки embedded платежа СБП', error=e)
await callback.answer('❌ Ошибка обработки платежа', show_alert=True)
@@ -705,7 +703,7 @@ async def handle_quick_amount_selection(callback: types.CallbackQuery, db_user:
except ValueError:
await callback.answer('❌ Ошибка обработки суммы', show_alert=True)
except Exception as e:
- logger.error(f'Ошибка обработки быстрого выбора суммы: {e}')
+ logger.error('Ошибка обработки быстрого выбора суммы', error=e)
await callback.answer('❌ Ошибка обработки запроса', show_alert=True)
@@ -755,7 +753,7 @@ async def handle_topup_amount_callback(
await callback.answer()
except Exception as error:
- logger.error(f'Ошибка быстрого пополнения: {error}')
+ logger.error('Ошибка быстрого пополнения', error=error)
await callback.answer('❌ Ошибка обработки запроса', show_alert=True)
diff --git a/app/handlers/balance/mulenpay.py b/app/handlers/balance/mulenpay.py
index c6470bdb..34d470b8 100644
--- a/app/handlers/balance/mulenpay.py
+++ b/app/handlers/balance/mulenpay.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +12,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -144,16 +143,13 @@ async def process_mulenpay_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - depends on bot permissions
- logger.warning('Не удалось удалить сообщение с суммой MulenPay: %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой MulenPay', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы MulenPay: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы MulenPay', delete_error=delete_error)
try:
payment_service = PaymentService(message.bot)
@@ -247,7 +243,7 @@ async def process_mulenpay_payment_amount(
metadata=payment_metadata,
)
except Exception as error: # pragma: no cover - diagnostic logging only
- logger.warning('Не удалось сохранить данные сообщения MulenPay: %s', error)
+ logger.warning('Не удалось сохранить данные сообщения MulenPay', error=error)
await state.update_data(
mulenpay_invoice_message_id=invoice_message.message_id,
@@ -257,15 +253,15 @@ async def process_mulenpay_payment_amount(
await state.clear()
logger.info(
- 'Создан %s платеж для пользователя %s: %s₽, ID: %s',
- mulenpay_name,
- db_user.telegram_id,
- amount_rubles,
- payment_id_display,
+ 'Создан платеж для пользователя : ₽, ID',
+ mulenpay_name=mulenpay_name,
+ telegram_id=db_user.telegram_id,
+ amount_rubles=amount_rubles,
+ payment_id_display=payment_id_display,
)
except Exception as e:
- logger.error(f'Ошибка создания {mulenpay_name} платежа: {e}')
+ logger.error('Ошибка создания платежа', mulenpay_name=mulenpay_name, error=e)
await message.answer(
texts.t(
'MULENPAY_PAYMENT_ERROR',
@@ -329,5 +325,5 @@ async def check_mulenpay_payment_status(callback: types.CallbackQuery, db: Async
await callback.answer(message_text, show_alert=True)
except Exception as e:
- logger.error(f'Ошибка проверки статуса {settings.get_mulenpay_display_name()}: {e}')
+ logger.error('Ошибка проверки статуса', get_mulenpay_display_name=settings.get_mulenpay_display_name(), error=e)
await callback.answer('❌ Ошибка проверки статуса', show_alert=True)
diff --git a/app/handlers/balance/pal24.py b/app/handlers/balance/pal24.py
index 39b1eb3b..5fcbf6ca 100644
--- a/app/handlers/balance/pal24.py
+++ b/app/handlers/balance/pal24.py
@@ -1,8 +1,8 @@
import html
-import logging
from datetime import datetime
from typing import Any
+import structlog
from aiogram import types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -19,7 +19,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _get_available_pal24_methods() -> list[str]:
@@ -224,7 +224,7 @@ async def _send_pal24_payment_message(
)
await db.commit()
except Exception as error: # pragma: no cover - diagnostics
- logger.warning('Не удалось сохранить сообщение PayPalych: %s', error)
+ logger.warning('Не удалось сохранить сообщение PayPalych', error=error)
await state.update_data(
pal24_invoice_message_id=invoice_message.message_id,
@@ -234,15 +234,15 @@ async def _send_pal24_payment_message(
await state.clear()
logger.info(
- 'Создан PayPalych счет для пользователя %s: %s₽, ID: %s, метод: %s',
- db_user.telegram_id,
- amount_kopeks / 100,
- bill_id,
- payment_method,
+ 'Создан PayPalych счет для пользователя : ₽, ID метод',
+ telegram_id=db_user.telegram_id,
+ amount_kopeks=amount_kopeks / 100,
+ bill_id=bill_id,
+ payment_method=payment_method,
)
except Exception as error:
- logger.error(f'Ошибка создания PayPalych платежа: {error}')
+ logger.error('Ошибка создания PayPalych платежа', error=error)
await message.answer(
texts.t(
'PAL24_PAYMENT_ERROR',
@@ -376,16 +376,13 @@ async def process_pal24_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning('Не удалось удалить сообщение с суммой PayPalych: %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой PayPalych', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы PayPalych: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы PayPalych', delete_error=delete_error)
if len(available_methods) == 1:
await _send_pal24_payment_message(
@@ -658,5 +655,5 @@ async def check_pal24_payment_status(
raise
except Exception as e:
- logger.error(f'Ошибка проверки статуса PayPalych: {e}')
+ logger.error('Ошибка проверки статуса PayPalych', error=e)
await callback.answer('❌ Ошибка проверки статуса', show_alert=True)
diff --git a/app/handlers/balance/platega.py b/app/handlers/balance/platega.py
index 79041ace..bad82ae0 100644
--- a/app/handlers/balance/platega.py
+++ b/app/handlers/balance/platega.py
@@ -1,7 +1,6 @@
"""Handlers for Platega balance interactions."""
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -15,7 +14,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _get_active_methods() -> list[int]:
@@ -275,7 +274,7 @@ async def process_platega_payment_amount(
payment_method_code=method_code,
)
except Exception as error:
- logger.exception('Ошибка создания платежа Platega: %s', error)
+ logger.exception('Ошибка создания платежа Platega', error=error)
payment_result = None
if not payment_result or not payment_result.get('redirect_url'):
@@ -336,16 +335,13 @@ async def process_platega_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning('Не удалось удалить сообщение с суммой Platega: %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой Platega', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - диагностический лог
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы Platega: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы Platega', delete_error=delete_error)
invoice_message = await message.answer(
instructions_template.format(
@@ -374,7 +370,7 @@ async def process_platega_payment_amount(
metadata=payment_metadata,
)
except Exception as error: # pragma: no cover - диагностический лог
- logger.warning('Не удалось сохранить данные сообщения Platega: %s', error)
+ logger.warning('Не удалось сохранить данные сообщения Platega', error=error)
await state.update_data(
platega_invoice_message_id=invoice_message.message_id,
@@ -400,7 +396,7 @@ async def check_platega_payment_status(
try:
status_info = await payment_service.get_platega_payment_status(db, local_payment_id)
except Exception as error:
- logger.exception('Ошибка проверки статуса Platega: %s', error)
+ logger.exception('Ошибка проверки статуса Platega', error=error)
await callback.answer('⚠️ Ошибка проверки статуса', show_alert=True)
return
diff --git a/app/handlers/balance/stars.py b/app/handlers/balance/stars.py
index 6d0135f0..13ef433c 100644
--- a/app/handlers/balance/stars.py
+++ b/app/handlers/balance/stars.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -13,7 +12,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -126,16 +125,13 @@ async def process_stars_payment_amount(message: types.Message, db_user: User, am
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning('Не удалось удалить сообщение с суммой Stars: %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой Stars', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - диагностический лог
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы Stars: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы Stars', delete_error=delete_error)
invoice_message = await message.answer(
f'⭐ Оплата через Telegram Stars\n\n'
@@ -155,5 +151,5 @@ async def process_stars_payment_amount(message: types.Message, db_user: User, am
await state.set_state(None)
except Exception as e:
- logger.error(f'Ошибка создания Stars invoice: {e}')
+ logger.error('Ошибка создания Stars invoice', error=e)
await message.answer('⚠️ Ошибка создания платежа')
diff --git a/app/handlers/balance/tribute.py b/app/handlers/balance/tribute.py
index 425fd764..973eb2f3 100644
--- a/app/handlers/balance/tribute.py
+++ b/app/handlers/balance/tribute.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import types
from app.config import settings
@@ -8,7 +7,7 @@ from app.localization.texts import get_texts
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -83,7 +82,7 @@ async def start_tribute_payment(
)
except Exception as e:
- logger.error(f'Ошибка создания Tribute платежа: {e}')
+ logger.error('Ошибка создания Tribute платежа', error=e)
await callback.answer('❌ Ошибка создания платежа', show_alert=True)
await callback.answer()
diff --git a/app/handlers/balance/wata.py b/app/handlers/balance/wata.py
index bfce4090..4682747d 100644
--- a/app/handlers/balance/wata.py
+++ b/app/handlers/balance/wata.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy import update
@@ -15,7 +15,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -144,7 +144,7 @@ async def process_wata_payment_amount(
language=db_user.language,
)
except Exception as error: # pragma: no cover - handled by decorator logs
- logger.exception('Ошибка создания WATA платежа: %s', error)
+ logger.exception('Ошибка создания WATA платежа', error=error)
result = None
if not result or not result.get('payment_url'):
@@ -207,16 +207,13 @@ async def process_wata_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning('Не удалось удалить сообщение с суммой WATA: %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой WATA', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы WATA: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы WATA', delete_error=delete_error)
invoice_message = await message.answer(
message_text,
@@ -241,7 +238,7 @@ async def process_wata_payment_amount(
)
await db.commit()
except Exception as error: # pragma: no cover - diagnostics
- logger.warning('Не удалось сохранить сообщение WATA: %s', error)
+ logger.warning('Не удалось сохранить сообщение WATA', error=error)
await state.update_data(
wata_invoice_message_id=invoice_message.message_id,
@@ -251,10 +248,10 @@ async def process_wata_payment_amount(
await state.clear()
logger.info(
- 'Создан WATA платеж для пользователя %s: %s₽, ссылка: %s',
- db_user.telegram_id,
- amount_kopeks / 100,
- payment_link_id,
+ 'Создан WATA платеж для пользователя : ₽, ссылка',
+ telegram_id=db_user.telegram_id,
+ amount_kopeks=amount_kopeks / 100,
+ payment_link_id=payment_link_id,
)
@@ -284,7 +281,7 @@ async def check_wata_payment_status(
if user and getattr(user, 'language', None):
user_language = user.language
except Exception as error:
- logger.debug('Не удалось получить пользователя для WATA статуса: %s', error)
+ logger.debug('Не удалось получить пользователя для WATA статуса', error=error)
texts = get_texts(user_language)
diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py
index 7ae59386..74e65c83 100644
--- a/app/handlers/balance/yookassa.py
+++ b/app/handlers/balance/yookassa.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy import update
@@ -15,7 +15,7 @@ from app.states import BalanceStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -233,16 +233,13 @@ async def process_yookassa_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning('Не удалось удалить сообщение с суммой YooKassa: %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой YooKassa', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - диагностический лог
- logger.warning(
- 'Не удалось удалить сообщение с запросом суммы YooKassa: %s',
- delete_error,
- )
+ logger.warning('Не удалось удалить сообщение с запросом суммы YooKassa', delete_error=delete_error)
invoice_message = await message.answer(
f'💳 Оплата банковской картой\n\n'
@@ -277,7 +274,7 @@ async def process_yookassa_payment_amount(
)
await db.commit()
except Exception as error: # pragma: no cover - диагностический лог
- logger.warning('Не удалось сохранить сообщение YooKassa: %s', error)
+ logger.warning('Не удалось сохранить сообщение YooKassa', error=error)
await state.update_data(
yookassa_invoice_message_id=invoice_message.message_id,
@@ -286,12 +283,14 @@ async def process_yookassa_payment_amount(
await state.clear()
logger.info(
- f'Создан платеж YooKassa для пользователя {db_user.telegram_id}: '
- f'{amount_kopeks // 100}₽, ID: {payment_result["yookassa_payment_id"]}'
+ 'Создан платеж YooKassa для пользователя ₽, ID',
+ telegram_id=db_user.telegram_id,
+ value=amount_kopeks // 100,
+ payment_result=payment_result['yookassa_payment_id'],
)
except Exception as e:
- logger.error(f'Ошибка создания YooKassa платежа: {e}')
+ logger.error('Ошибка создания YooKassa платежа', error=e)
await message.answer('❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.')
await state.clear()
@@ -392,7 +391,7 @@ async def process_yookassa_sbp_payment_amount(
except ImportError:
logger.warning('qrcode библиотека не установлена, QR-код не будет сгенерирован')
except Exception as e:
- logger.error(f'Ошибка генерации QR-кода: {e}')
+ logger.error('Ошибка генерации QR-кода', error=e)
# Если нет QR-данных из YooKassa, но есть URL, генерируем QR-код из URL
if not qr_photo and confirmation_url:
@@ -419,7 +418,7 @@ async def process_yookassa_sbp_payment_amount(
except ImportError:
logger.warning('qrcode библиотека не установлена, QR-код не будет сгенерирован')
except Exception as e:
- logger.error(f'Ошибка генерации QR-кода из URL: {e}')
+ logger.error('Ошибка генерации QR-кода из URL', error=e)
# Создаем клавиатуру с кнопками для оплаты по ссылке и проверки статуса
keyboard_buttons = []
@@ -452,15 +451,14 @@ async def process_yookassa_sbp_payment_amount(
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning('Не удалось удалить сообщение с суммой YooKassa (СБП): %s', delete_error)
+ logger.warning('Не удалось удалить сообщение с суммой YooKassa (СБП)', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - диагностический лог
logger.warning(
- 'Не удалось удалить сообщение с запросом суммы YooKassa (СБП): %s',
- delete_error,
+ 'Не удалось удалить сообщение с запросом суммы YooKassa (СБП)', delete_error=delete_error
)
# Подготавливаем текст сообщения
@@ -515,7 +513,7 @@ async def process_yookassa_sbp_payment_amount(
)
await db.commit()
except Exception as error: # pragma: no cover - диагностический лог
- logger.warning('Не удалось сохранить сообщение YooKassa (СБП): %s', error)
+ logger.warning('Не удалось сохранить сообщение YooKassa (СБП)', error=error)
await state.update_data(
yookassa_invoice_message_id=invoice_message.message_id,
@@ -524,12 +522,14 @@ async def process_yookassa_sbp_payment_amount(
await state.clear()
logger.info(
- f'Создан платеж YooKassa СБП для пользователя {db_user.telegram_id}: '
- f'{amount_kopeks // 100}₽, ID: {payment_result["yookassa_payment_id"]}'
+ 'Создан платеж YooKassa СБП для пользователя ₽, ID',
+ telegram_id=db_user.telegram_id,
+ value=amount_kopeks // 100,
+ payment_result=payment_result['yookassa_payment_id'],
)
except Exception as e:
- logger.error(f'Ошибка создания YooKassa СБП платежа: {e}')
+ logger.error('Ошибка создания YooKassa СБП платежа', error=e)
await message.answer('❌ Ошибка создания платежа через СБП. Попробуйте позже или обратитесь в поддержку.')
await state.clear()
@@ -584,5 +584,5 @@ async def check_yookassa_payment_status(callback: types.CallbackQuery, db: Async
await callback.answer(message_text, show_alert=True)
except Exception as e:
- logger.error(f'Ошибка проверки статуса платежа: {e}')
+ logger.error('Ошибка проверки статуса платежа', error=e)
await callback.answer('❌ Ошибка проверки статуса', show_alert=True)
diff --git a/app/handlers/common.py b/app/handlers/common.py
index e35b68a7..4ee419ce 100644
--- a/app/handlers/common.py
+++ b/app/handlers/common.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
@@ -10,7 +9,7 @@ from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_rules, get_texts
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def handle_delete_ban_notification(
@@ -21,7 +20,7 @@ async def handle_delete_ban_notification(
await callback.message.delete()
await callback.answer('Уведомление удалено')
except Exception as e:
- logger.warning(f'Не удалось удалить уведомление: {e}')
+ logger.warning('Не удалось удалить уведомление', error=e)
await callback.answer('Не удалось удалить', show_alert=False)
@@ -36,7 +35,7 @@ async def handle_webhook_notification_close(
try:
await callback.message.delete()
except Exception as e:
- logger.warning('Не удалось удалить webhook-уведомление: %s', e)
+ logger.warning('Не удалось удалить webhook-уведомление', e=e)
try:
await callback.message.edit_reply_markup(reply_markup=None)
except Exception:
@@ -54,7 +53,9 @@ async def handle_unknown_callback(callback: types.CallbackQuery, db_user: User):
show_alert=True,
)
- logger.warning(f'Неизвестный callback: {callback.data} от пользователя {callback.from_user.id}')
+ logger.warning(
+ 'Неизвестный callback: от пользователя', callback_data=callback.data, from_user_id=callback.from_user.id
+ )
async def handle_noop(callback: types.CallbackQuery, db_user: User):
diff --git a/app/handlers/contests.py b/app/handlers/contests.py
index 752be504..11d58e20 100644
--- a/app/handlers/contests.py
+++ b/app/handlers/contests.py
@@ -1,8 +1,8 @@
"""Contest handlers for daily games."""
-import logging
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
@@ -22,7 +22,7 @@ from app.states import ContestStates
from app.utils.decorators import auth_required, error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Rate limiting storage
_rate_limits: dict = {}
diff --git a/app/handlers/menu.py b/app/handlers/menu.py
index 830e882a..1512891c 100644
--- a/app/handlers/menu.py
+++ b/app/handlers/menu.py
@@ -1,8 +1,8 @@
import html
-import logging
from datetime import datetime
from decimal import Decimal
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
@@ -43,7 +43,7 @@ from app.utils.promo_offer import (
from app.utils.timezone import format_local_datetime
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _format_rubles(amount_kopeks: int) -> str:
@@ -183,7 +183,7 @@ async def show_main_menu(
try:
has_saved_cart = await user_cart_service.has_user_cart(db_user.id)
except Exception as e:
- logger.error(f'Ошибка проверки сохраненной корзины для пользователя {db_user.id}: {e}')
+ logger.error('Ошибка проверки сохраненной корзины для пользователя', db_user_id=db_user.id, error=e)
has_saved_cart = False
is_admin = settings.is_admin(db_user.telegram_id)
@@ -1029,7 +1029,7 @@ async def handle_back_to_menu(callback: types.CallbackQuery, state: FSMContext,
try:
has_saved_cart = await user_cart_service.has_user_cart(db_user.id)
except Exception as e:
- logger.error(f'Ошибка проверки сохраненной корзины для пользователя {db_user.id}: {e}')
+ logger.error('Ошибка проверки сохраненной корзины для пользователя', db_user_id=db_user.id, error=e)
has_saved_cart = False
is_admin = settings.is_admin(db_user.telegram_id)
@@ -1182,7 +1182,7 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
# Формируем краткий блок информации о тарифе для главного меню
tariff_info_block = f'\n📦 Тариф: {tariff.name}'
except Exception as e:
- logger.debug(f'Не удалось загрузить тариф для главного меню: {e}')
+ logger.debug('Не удалось загрузить тариф для главного меню', error=e)
base_text = texts.MAIN_MENU.format(
user_name=user.full_name, subscription_status=_get_subscription_status(user, texts, is_daily_tariff)
@@ -1204,9 +1204,9 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
info_sections.append(promo_hint.strip())
except Exception as hint_error:
logger.debug(
- 'Не удалось построить подсказку промо-предложения для пользователя %s: %s',
- getattr(user, 'id', None),
- hint_error,
+ 'Не удалось построить подсказку промо-предложения для пользователя',
+ getattr=getattr(user, 'id', None),
+ hint_error=hint_error,
)
try:
@@ -1215,9 +1215,9 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
info_sections.append(test_access_hint.strip())
except Exception as test_error:
logger.debug(
- 'Не удалось построить подсказку тестового доступа для пользователя %s: %s',
- getattr(user, 'id', None),
- test_error,
+ 'Не удалось построить подсказку тестового доступа для пользователя',
+ getattr=getattr(user, 'id', None),
+ test_error=test_error,
)
if info_sections:
@@ -1231,7 +1231,7 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
return _insert_random_message(base_text, random_message, action_prompt)
except Exception as e:
- logger.error(f'Ошибка получения случайного сообщения: {e}')
+ logger.error('Ошибка получения случайного сообщения', error=e)
return base_text
@@ -1373,7 +1373,7 @@ async def handle_activate_button(callback: types.CallbackQuery, db_user: User, d
except Exception as e:
user_id_display = db_user.telegram_id or db_user.email or f'#{db_user.id}'
- logger.error(f'Ошибка автоматической активации для {user_id_display}: {e}')
+ logger.error('Ошибка автоматической активации для', user_id_display=user_id_display, error=e)
await db.rollback()
await callback.answer(
texts.t('ACTIVATION_ERROR', '❌ Ошибка активации. Попробуйте позже.'),
diff --git a/app/handlers/polls.py b/app/handlers/polls.py
index dfb35e44..fef6f778 100644
--- a/app/handlers/polls.py
+++ b/app/handlers/polls.py
@@ -1,7 +1,7 @@
import asyncio
-import logging
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +16,7 @@ from app.localization.texts import get_texts
from app.services.poll_service import get_next_question, get_question_option, reward_user_for_poll
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _delete_message_later(bot, chat_id: int, message_id: int, delay: int = 10) -> None:
@@ -24,7 +24,7 @@ async def _delete_message_later(bot, chat_id: int, message_id: int, delay: int =
await asyncio.sleep(delay)
await bot.delete_message(chat_id, message_id)
except Exception as error: # pragma: no cover - cleanup best effort
- logger.debug('Не удалось удалить сообщение опроса %s: %s', message_id, error)
+ logger.debug('Не удалось удалить сообщение опроса', message_id=message_id, error=error)
async def _render_question_text(
@@ -60,22 +60,13 @@ async def _update_poll_message(
except TelegramBadRequest as error:
error_text = str(error).lower()
if 'message is not modified' in error_text:
- logger.debug(
- 'Опросное сообщение уже актуально, пропускаем обновление: %s',
- error,
- )
+ logger.debug('Опросное сообщение уже актуально, пропускаем обновление', error=error)
return True
- logger.warning(
- 'Не удалось обновить сообщение опроса %s: %s',
- message.message_id,
- error,
- )
+ logger.warning('Не удалось обновить сообщение опроса', message_id=message.message_id, error=error)
except Exception as error: # pragma: no cover - defensive logging
logger.exception(
- 'Непредвиденная ошибка при обновлении сообщения опроса %s: %s',
- message.message_id,
- error,
+ 'Непредвиденная ошибка при обновлении сообщения опроса', message_id=message.message_id, error=error
)
return False
@@ -197,11 +188,7 @@ async def handle_poll_answer(
try:
await db.refresh(response, attribute_names=['answers'])
except Exception as error: # pragma: no cover - defensive cache busting
- logger.debug(
- 'Не удалось обновить локальные ответы опроса %s: %s',
- response.id,
- error,
- )
+ logger.debug('Не удалось обновить локальные ответы опроса', response_id=response.id, error=error)
response = await get_poll_response_by_id(db, response.id)
if not response:
await callback.answer(texts.t('POLL_ERROR', 'Опрос недоступен.'), show_alert=True)
diff --git a/app/handlers/promocode.py b/app/handlers/promocode.py
index 3beefc43..a57c85f3 100644
--- a/app/handlers/promocode.py
+++ b/app/handlers/promocode.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Bot, Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from aiogram.types import InaccessibleMessage
@@ -14,7 +13,7 @@ from app.states import PromoCodeStates
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -40,7 +39,7 @@ async def activate_promocode_for_registration(db: AsyncSession, user_id: int, co
result = await promocode_service.activate_promocode(db, user_id, code)
if result['success']:
- logger.info(f'✅ Пользователь {user_id} активировал промокод {code} при регистрации')
+ logger.info('✅ Пользователь активировал промокод при регистрации', user_id=user_id, code=code)
# Отправляем уведомление админу, если бот доступен
if bot:
@@ -60,9 +59,7 @@ async def activate_promocode_for_registration(db: AsyncSession, user_id: int, co
)
except Exception as notify_error:
logger.error(
- 'Ошибка отправки админ уведомления об активации промокода %s: %s',
- code,
- notify_error,
+ 'Ошибка отправки админ уведомления об активации промокода', code=code, notify_error=notify_error
)
return result
diff --git a/app/handlers/referral.py b/app/handlers/referral.py
index 58d3e6b7..50701f7c 100644
--- a/app/handlers/referral.py
+++ b/app/handlers/referral.py
@@ -1,8 +1,8 @@
import json
-import logging
from pathlib import Path
import qrcode
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -25,7 +25,7 @@ from app.utils.user_utils import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def show_referral_info(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
@@ -795,7 +795,7 @@ async def confirm_withdrawal_request(callback: types.CallbackQuery, db_user: Use
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_to_admins(admin_text, keyboard=admin_keyboard)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления админам о заявке на вывод: {e}')
+ logger.error('Ошибка отправки уведомления админам о заявке на вывод', error=e)
# Уведомление в топик, если настроено
topic_id = settings.REFERRAL_WITHDRAWAL_NOTIFICATIONS_TOPIC_ID
@@ -809,7 +809,7 @@ async def confirm_withdrawal_request(callback: types.CallbackQuery, db_user: Use
parse_mode='HTML',
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления в топик о заявке на вывод: {e}')
+ logger.error('Ошибка отправки уведомления в топик о заявке на вывод', error=e)
# Отвечаем пользователю
text = texts.t(
diff --git a/app/handlers/server_status.py b/app/handlers/server_status.py
index 9f527a23..858c8337 100644
--- a/app/handlers/server_status.py
+++ b/app/handlers/server_status.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from aiogram import Dispatcher, F, types
from app.config import settings
@@ -14,7 +14,7 @@ from app.services.server_status_service import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
_status_service = ServerStatusService()
@@ -47,14 +47,14 @@ async def _render_server_status(
try:
servers = await _status_service.get_servers()
except ServerStatusError as error:
- logger.warning('Server status error: %s', error)
+ logger.warning('Server status error', error=error)
await callback.answer(
texts.t('SERVER_STATUS_ERROR_SHORT', 'Не удалось получить данные'),
show_alert=True,
)
return
except Exception as error: # pragma: no cover - defensive logging
- logger.error('Unexpected server status error: %s', error)
+ logger.error('Unexpected server status error', error=error)
await callback.answer(
texts.t('SERVER_STATUS_ERROR_SHORT', 'Не удалось получить данные'),
show_alert=True,
diff --git a/app/handlers/simple_subscription.py b/app/handlers/simple_subscription.py
index 095fd2c8..11f5f7cb 100644
--- a/app/handlers/simple_subscription.py
+++ b/app/handlers/simple_subscription.py
@@ -1,10 +1,10 @@
"""Обработчики для простой покупки подписки."""
import html
-import logging
from datetime import datetime
from typing import Any
+import structlog
from aiogram import F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -25,7 +25,7 @@ from app.utils.subscription_utils import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@error_handler
@@ -120,11 +120,11 @@ async def start_simple_subscription_purchase(
can_pay_from_balance = user_balance_kopeks >= price_kopeks
logger.warning(
- 'SIMPLE_SUBSCRIPTION_DEBUG_START_BALANCE | user=%s | balance=%s | min_required=%s | can_pay=%s',
- db_user.id,
- user_balance_kopeks,
- price_kopeks,
- can_pay_from_balance,
+ 'SIMPLE_SUBSCRIPTION_DEBUG_START_BALANCE | user= | balance= | min_required= | can_pay',
+ db_user_id=db_user.id,
+ user_balance_kopeks=user_balance_kopeks,
+ price_kopeks=price_kopeks,
+ can_pay_from_balance=can_pay_from_balance,
)
# Проверяем, является ли у пользователя текущая подписка активной платной подпиской
@@ -352,19 +352,13 @@ async def _ensure_simple_subscription_squad_uuid(
resolved_uuid = await get_random_active_squad_uuid(db)
except Exception as error: # pragma: no cover - defensive logging
- logger.error(
- 'SIMPLE_SUBSCRIPTION_RANDOM_SQUAD_ERROR | user=%s | error=%s',
- user_id,
- error,
- )
+ logger.error('SIMPLE_SUBSCRIPTION_RANDOM_SQUAD_ERROR | user= | error', user_id=user_id, error=error)
return None
if resolved_uuid:
await state.update_data(resolved_squad_uuid=resolved_uuid)
logger.info(
- 'SIMPLE_SUBSCRIPTION_RANDOM_SQUAD_ASSIGNED | user=%s | squad=%s',
- user_id,
- resolved_uuid,
+ 'SIMPLE_SUBSCRIPTION_RANDOM_SQUAD_ASSIGNED | user= | squad', user_id=user_id, resolved_uuid=resolved_uuid
)
return resolved_uuid
@@ -416,16 +410,16 @@ async def handle_simple_subscription_pay_with_balance(
)
total_required = price_kopeks
logger.warning(
- 'SIMPLE_SUBSCRIPTION_DEBUG_PAY_BALANCE | user=%s | period=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | balance=%s',
- db_user.id,
- subscription_params['period_days'],
- price_breakdown.get('base_price', 0),
- price_breakdown.get('traffic_price', 0),
- price_breakdown.get('devices_price', 0),
- price_breakdown.get('servers_price', 0),
- price_breakdown.get('total_discount', 0),
- total_required,
- getattr(db_user, 'balance_kopeks', 0),
+ 'SIMPLE_SUBSCRIPTION_DEBUG_PAY_BALANCE | user= | period= | base= | traffic= | devices= | servers= | discount= | total_required= | balance',
+ db_user_id=db_user.id,
+ subscription_params=subscription_params['period_days'],
+ price_breakdown=price_breakdown.get('base_price', 0),
+ price_breakdown_2=price_breakdown.get('traffic_price', 0),
+ price_breakdown_3=price_breakdown.get('devices_price', 0),
+ price_breakdown_4=price_breakdown.get('servers_price', 0),
+ price_breakdown_5=price_breakdown.get('total_discount', 0),
+ total_required=total_required,
+ getattr=getattr(db_user, 'balance_kopeks', 0),
)
# Проверяем баланс пользователя
@@ -523,7 +517,10 @@ async def handle_simple_subscription_pay_with_balance(
await db.refresh(subscription)
except Exception as sync_error:
logger.error(
- f'Ошибка синхронизации подписки с RemnaWave для пользователя {db_user.id}: {sync_error}', exc_info=True
+ 'Ошибка синхронизации подписки с RemnaWave для пользователя',
+ db_user_id=db_user.id,
+ sync_error=sync_error,
+ exc_info=True,
)
# Отправляем уведомление об успешной покупке
@@ -641,18 +638,22 @@ async def handle_simple_subscription_pay_with_balance(
amount_kopeks=price_kopeks,
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления админам о покупке: {e}')
+ logger.error('Ошибка отправки уведомления админам о покупке', error=e)
await state.clear()
await callback.answer()
- logger.info(f'Пользователь {db_user.telegram_id} успешно купил подписку с баланса на {price_kopeks / 100}₽')
+ logger.info(
+ 'Пользователь успешно купил подписку с баланса на ₽',
+ telegram_id=db_user.telegram_id,
+ price_kopeks=price_kopeks / 100,
+ )
except Exception as error:
logger.error(
- 'Ошибка оплаты простой подписки с баланса для пользователя %s: %s',
- db_user.id,
- error,
+ 'Ошибка оплаты простой подписки с баланса для пользователя',
+ db_user_id=db_user.id,
+ error=error,
exc_info=True,
)
await callback.answer(
@@ -712,16 +713,16 @@ async def handle_simple_subscription_other_payment_methods(
user_balance_kopeks = getattr(db_user, 'balance_kopeks', 0)
can_pay_from_balance = user_balance_kopeks >= price_kopeks
logger.warning(
- 'SIMPLE_SUBSCRIPTION_DEBUG_METHODS | user=%s | balance=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | can_pay=%s',
- db_user.id,
- user_balance_kopeks,
- price_breakdown.get('base_price', 0),
- price_breakdown.get('traffic_price', 0),
- price_breakdown.get('devices_price', 0),
- price_breakdown.get('servers_price', 0),
- price_breakdown.get('total_discount', 0),
- price_kopeks,
- can_pay_from_balance,
+ 'SIMPLE_SUBSCRIPTION_DEBUG_METHODS | user= | balance= | base= | traffic= | devices= | servers= | discount= | total_required= | can_pay',
+ db_user_id=db_user.id,
+ user_balance_kopeks=user_balance_kopeks,
+ price_breakdown=price_breakdown.get('base_price', 0),
+ price_breakdown_2=price_breakdown.get('traffic_price', 0),
+ price_breakdown_3=price_breakdown.get('devices_price', 0),
+ price_breakdown_4=price_breakdown.get('servers_price', 0),
+ price_breakdown_5=price_breakdown.get('total_discount', 0),
+ price_kopeks=price_kopeks,
+ can_pay_from_balance=can_pay_from_balance,
)
# Отображаем доступные методы оплаты
@@ -974,7 +975,7 @@ async def handle_simple_subscription_payment_method(
except ImportError:
logger.warning('qrcode библиотека не установлена, QR-код не будет сгенерирован')
except Exception as e:
- logger.error(f'Ошибка генерации QR-кода: {e}')
+ logger.error('Ошибка генерации QR-кода', error=e)
# Создаем клавиатуру с кнопками для оплаты по ссылке и проверки статуса
keyboard_buttons = []
@@ -1079,7 +1080,7 @@ async def handle_simple_subscription_payment_method(
usd_rate = await currency_converter.get_usd_to_rub_rate()
except Exception as rate_error:
- logger.warning('Не удалось получить курс USD: %s', rate_error)
+ logger.warning('Не удалось получить курс USD', rate_error=rate_error)
usd_rate = 95.0
amount_usd = round(amount_rubles / usd_rate, 2)
@@ -1576,7 +1577,7 @@ async def handle_simple_subscription_payment_method(
language=db_user.language,
)
except Exception as error:
- logger.error('Ошибка создания WATA платежа: %s', error)
+ logger.error('Ошибка создания WATA платежа', error=error)
wata_result = None
if not wata_result or not wata_result.get('payment_url'):
@@ -1644,7 +1645,7 @@ async def handle_simple_subscription_payment_method(
await callback.answer('❌ Неизвестный способ оплаты', show_alert=True)
except Exception as e:
- logger.error(f'Ошибка обработки метода оплаты простой подписки: {e}')
+ logger.error('Ошибка обработки метода оплаты простой подписки', error=e)
await callback.answer(
'❌ Ошибка обработки запроса. Попробуйте позже или обратитесь в поддержку.', show_alert=True
)
@@ -1781,7 +1782,7 @@ async def check_simple_pal24_payment_status(
raise
except Exception as error:
- logger.error(f'Ошибка проверки статуса PayPalych для простой подписки: {error}')
+ logger.error('Ошибка проверки статуса PayPalych для простой подписки', error=error)
await callback.answer('❌ Ошибка проверки статуса', show_alert=True)
@@ -1813,7 +1814,7 @@ async def check_simple_mulenpay_payment_status(
if user and getattr(user, 'language', None):
user_language = user.language
except Exception as error:
- logger.debug('Не удалось получить пользователя для MulenPay статуса: %s', error)
+ logger.debug('Не удалось получить пользователя для MulenPay статуса', error=error)
texts = get_texts(user_language)
status_labels = {
@@ -1895,7 +1896,7 @@ async def check_simple_cryptobot_payment_status(
if user and getattr(user, 'language', None):
language = user.language
except Exception as error:
- logger.debug('Не удалось получить пользователя для CryptoBot статуса: %s', error)
+ logger.debug('Не удалось получить пользователя для CryptoBot статуса', error=error)
texts = get_texts(language)
message_lines = [
@@ -1971,7 +1972,7 @@ async def check_simple_heleket_payment_status(
if user and getattr(user, 'language', None):
language = user.language
except Exception as error:
- logger.debug('Не удалось получить пользователя для Heleket статуса: %s', error)
+ logger.debug('Не удалось получить пользователя для Heleket статуса', error=error)
texts = get_texts(language)
@@ -2115,16 +2116,16 @@ async def confirm_simple_subscription_purchase(
)
total_required = price_kopeks
logger.warning(
- 'SIMPLE_SUBSCRIPTION_DEBUG_CONFIRM | user=%s | period=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | balance=%s',
- db_user.id,
- subscription_params['period_days'],
- price_breakdown.get('base_price', 0),
- price_breakdown.get('traffic_price', 0),
- price_breakdown.get('devices_price', 0),
- price_breakdown.get('servers_price', 0),
- price_breakdown.get('total_discount', 0),
- total_required,
- getattr(db_user, 'balance_kopeks', 0),
+ 'SIMPLE_SUBSCRIPTION_DEBUG_CONFIRM | user= | period= | base= | traffic= | devices= | servers= | discount= | total_required= | balance',
+ db_user_id=db_user.id,
+ subscription_params=subscription_params['period_days'],
+ price_breakdown=price_breakdown.get('base_price', 0),
+ price_breakdown_2=price_breakdown.get('traffic_price', 0),
+ price_breakdown_3=price_breakdown.get('devices_price', 0),
+ price_breakdown_4=price_breakdown.get('servers_price', 0),
+ price_breakdown_5=price_breakdown.get('total_discount', 0),
+ total_required=total_required,
+ getattr=getattr(db_user, 'balance_kopeks', 0),
)
# Проверяем баланс пользователя
@@ -2222,7 +2223,10 @@ async def confirm_simple_subscription_purchase(
await db.refresh(subscription)
except Exception as sync_error:
logger.error(
- f'Ошибка синхронизации подписки с RemnaWave для пользователя {db_user.id}: {sync_error}', exc_info=True
+ 'Ошибка синхронизации подписки с RemnaWave для пользователя',
+ db_user_id=db_user.id,
+ sync_error=sync_error,
+ exc_info=True,
)
# Отправляем уведомление об успешной покупке
@@ -2340,18 +2344,22 @@ async def confirm_simple_subscription_purchase(
amount_kopeks=price_kopeks,
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления админам о покупке: {e}')
+ logger.error('Ошибка отправки уведомления админам о покупке', error=e)
await state.clear()
await callback.answer()
- logger.info(f'Пользователь {db_user.telegram_id} успешно купил подписку с баланса на {price_kopeks / 100}₽')
+ logger.info(
+ 'Пользователь успешно купил подписку с баланса на ₽',
+ telegram_id=db_user.telegram_id,
+ price_kopeks=price_kopeks / 100,
+ )
except Exception as error:
logger.error(
- 'Ошибка подтверждения простой подписки с баланса для пользователя %s: %s',
- db_user.id,
- error,
+ 'Ошибка подтверждения простой подписки с баланса для пользователя',
+ db_user_id=db_user.id,
+ error=error,
exc_info=True,
)
await callback.answer(
diff --git a/app/handlers/stars_payments.py b/app/handlers/stars_payments.py
index a55d4efe..5373e713 100644
--- a/app/handlers/stars_payments.py
+++ b/app/handlers/stars_payments.py
@@ -1,6 +1,6 @@
-import logging
from decimal import ROUND_HALF_UP, Decimal
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +13,7 @@ from app.localization.texts import get_texts
from app.services.payment_service import PaymentService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _handle_wheel_spin_payment(
@@ -70,8 +70,10 @@ async def _handle_wheel_spin_payment(
promocode_id = row[0]
logger.info(
- f'🎰 Creating wheel spin: user.id={user.id}, user.telegram_id={user.telegram_id}, '
- f'prize={selected_prize.display_name}'
+ '🎰 Creating wheel spin: user.id=, user.telegram_id=, prize',
+ user_id=user.id,
+ telegram_id=user.telegram_id,
+ display_name=selected_prize.display_name,
)
spin = await create_wheel_spin(
@@ -89,7 +91,7 @@ async def _handle_wheel_spin_payment(
is_applied=True,
)
- logger.info(f'🎰 Wheel spin created: spin.id={spin.id}, spin.user_id={spin.user_id}')
+ logger.info('🎰 Wheel spin created: spin.id=, spin.user_id', spin_id=spin.id, user_id=spin.user_id)
# Ensure all changes are committed (subscription days, traffic GB, etc.)
await db.commit()
@@ -107,12 +109,15 @@ async def _handle_wheel_spin_payment(
)
logger.info(
- f'🎰 Wheel spin via Stars: user={user.id}, prize={selected_prize.display_name}, stars={stars_amount}'
+ '🎰 Wheel spin via Stars: user=, prize=, stars',
+ user_id=user.id,
+ display_name=selected_prize.display_name,
+ stars_amount=stars_amount,
)
return True
except Exception as e:
- logger.error(f'Ошибка обработки wheel spin payment: {e}', exc_info=True)
+ logger.error('Ошибка обработки wheel spin payment', error=e, exc_info=True)
await message.answer(
'❌ Произошла ошибка при обработке спина. Обратитесь в поддержку.',
)
@@ -138,7 +143,7 @@ async def _handle_trial_payment(
# Парсим payload: trial_{subscription_id}
parts = payload.split('_')
if len(parts) < 2:
- logger.error(f'Невалидный trial payload: {payload}')
+ logger.error('Невалидный trial payload', payload=payload)
await message.answer(
'❌ Ошибка: неверный формат платежа. Обратитесь в поддержку.',
)
@@ -147,7 +152,7 @@ async def _handle_trial_payment(
try:
subscription_id = int(parts[1])
except ValueError:
- logger.error(f'Невалидный subscription_id в trial payload: {payload}')
+ logger.error('Невалидный subscription_id в trial payload', payload=payload)
await message.answer(
'❌ Ошибка: неверный ID подписки. Обратитесь в поддержку.',
)
@@ -177,7 +182,11 @@ async def _handle_trial_payment(
)
if not subscription:
- logger.error(f'Не удалось активировать триальную подписку {subscription_id} для пользователя {user.id}')
+ logger.error(
+ 'Не удалось активировать триальную подписку для пользователя',
+ subscription_id=subscription_id,
+ user_id=user.id,
+ )
# Возвращаем деньги на баланс
from app.database.crud.user import add_user_balance
@@ -198,7 +207,7 @@ async def _handle_trial_payment(
try:
await subscription_service.create_remnawave_user(db, subscription)
except Exception as rw_error:
- logger.error(f'Ошибка создания пользователя RemnaWave для триала: {rw_error}')
+ logger.error('Ошибка создания пользователя RemnaWave для триала', rw_error=rw_error)
# Не откатываем подписку, просто логируем - RemnaWave может быть временно недоступен
await db.commit()
@@ -214,7 +223,7 @@ async def _handle_trial_payment(
payment_method='Telegram Stars',
)
except Exception as admin_error:
- logger.warning(f'Ошибка отправки уведомления админам о триале: {admin_error}')
+ logger.warning('Ошибка отправки уведомления админам о триале', admin_error=admin_error)
# Отправляем сообщение пользователю
await message.answer(
@@ -227,13 +236,15 @@ async def _handle_trial_payment(
)
logger.info(
- f'✅ Платный триал активирован через Stars: user={user.id}, '
- f'subscription={subscription.id}, stars={stars_amount}'
+ '✅ Платный триал активирован через Stars: user=, subscription=, stars',
+ user_id=user.id,
+ subscription_id=subscription.id,
+ stars_amount=stars_amount,
)
return True
except Exception as e:
- logger.error(f'Ошибка обработки trial payment: {e}', exc_info=True)
+ logger.error('Ошибка обработки trial payment', error=e, exc_info=True)
await message.answer(
'❌ Произошла ошибка при активации пробной подписки. Обратитесь в поддержку.',
)
@@ -245,13 +256,16 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
try:
logger.info(
- f'📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}'
+ '📋 Pre-checkout query от XTR, payload',
+ from_user_id=query.from_user.id,
+ total_amount=query.total_amount,
+ invoice_payload=query.invoice_payload,
)
allowed_prefixes = ('balance_', 'admin_stars_test_', 'simple_sub_', 'wheel_spin_', 'trial_')
if not query.invoice_payload or not query.invoice_payload.startswith(allowed_prefixes):
- logger.warning(f'Невалидный payload: {query.invoice_payload}')
+ logger.warning('Невалидный payload', invoice_payload=query.invoice_payload)
await query.answer(
ok=False,
error_message=texts.t(
@@ -267,7 +281,7 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
async with AsyncSessionLocal() as db:
user = await get_user_by_telegram_id(db, query.from_user.id)
if not user:
- logger.warning(f'Пользователь {query.from_user.id} не найден в БД')
+ logger.warning('Пользователь не найден в БД', from_user_id=query.from_user.id)
await query.answer(
ok=False,
error_message=texts.t(
@@ -278,7 +292,7 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
return
texts = get_texts(user.language or DEFAULT_LANGUAGE)
except Exception as db_error:
- logger.error(f'Ошибка подключения к БД в pre_checkout_query: {db_error}')
+ logger.error('Ошибка подключения к БД в pre_checkout_query', db_error=db_error)
await query.answer(
ok=False,
error_message=texts.t(
@@ -289,10 +303,10 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
return
await query.answer(ok=True)
- logger.info(f'✅ Pre-checkout одобрен для пользователя {query.from_user.id}')
+ logger.info('✅ Pre-checkout одобрен для пользователя', from_user_id=query.from_user.id)
except Exception as e:
- logger.error(f'Ошибка в pre_checkout_query: {e}', exc_info=True)
+ logger.error('Ошибка в pre_checkout_query', error=e, exc_info=True)
await query.answer(
ok=False,
error_message=texts.t(
@@ -310,17 +324,18 @@ async def handle_successful_payment(message: types.Message, db: AsyncSession, st
user_id = message.from_user.id
logger.info(
- f'💳 Успешный Stars платеж от {user_id}: '
- f'{payment.total_amount} XTR, '
- f'payload: {payment.invoice_payload}, '
- f'charge_id: {payment.telegram_payment_charge_id}'
+ '💳 Успешный Stars платеж от XTR, payload: charge_id',
+ user_id=user_id,
+ total_amount=payment.total_amount,
+ invoice_payload=payment.invoice_payload,
+ telegram_payment_charge_id=payment.telegram_payment_charge_id,
)
user = await get_user_by_telegram_id(db, user_id)
texts = get_texts(user.language if user and user.language else DEFAULT_LANGUAGE)
if not user:
- logger.error(f'Пользователь {user_id} не найден при обработке Stars платежа')
+ logger.error('Пользователь не найден при обработке Stars платежа', user_id=user_id)
await message.answer(
texts.t(
'STARS_PAYMENT_USER_NOT_FOUND',
@@ -370,9 +385,7 @@ async def handle_successful_payment(message: types.Message, db: AsyncSession, st
await message.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - зависит от прав бота
logger.warning(
- 'Не удалось удалить сообщение %s после оплаты Stars: %s',
- label,
- delete_error,
+ 'Не удалось удалить сообщение после оплаты Stars', label=label, delete_error=delete_error
)
success = await payment_service.process_stars_payment(
@@ -421,13 +434,13 @@ async def handle_successful_payment(message: types.Message, db: AsyncSession, st
)
logger.info(
- '✅ Stars платеж успешно обработан: пользователь %s, %s звезд → %s',
- user.id,
- payment.total_amount,
- settings.format_price(amount_kopeks),
+ '✅ Stars платеж успешно обработан: пользователь , звезд →',
+ user_id=user.id,
+ total_amount=payment.total_amount,
+ format_price=settings.format_price(amount_kopeks),
)
else:
- logger.error(f'Ошибка обработки Stars платежа для пользователя {user.id}')
+ logger.error('Ошибка обработки Stars платежа для пользователя', user_id=user.id)
await message.answer(
texts.t(
'STARS_PAYMENT_ENROLLMENT_ERROR',
@@ -437,7 +450,7 @@ async def handle_successful_payment(message: types.Message, db: AsyncSession, st
)
except Exception as e:
- logger.error(f'Ошибка в successful_payment: {e}', exc_info=True)
+ logger.error('Ошибка в successful_payment', error=e, exc_info=True)
await message.answer(
texts.t(
'STARS_PAYMENT_PROCESSING_ERROR',
diff --git a/app/handlers/start.py b/app/handlers/start.py
index 73542b45..7a42b6d7 100644
--- a/app/handlers/start.py
+++ b/app/handlers/start.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from aiogram import Bot, Dispatcher, F, types
from aiogram.enums import ChatMemberStatus
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
@@ -55,7 +55,7 @@ from app.utils.timezone import format_local_datetime
from app.utils.user_utils import generate_unique_referral_code
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _calculate_subscription_flags(subscription):
@@ -79,9 +79,9 @@ async def _send_pinned_message(
await deliver_pinned_message_to_user(bot, db, user, pinned_message)
except Exception as error:
logger.error(
- 'Не удалось отправить закрепленное сообщение пользователю %s: %s',
- getattr(user, 'telegram_id', 'unknown'),
- error,
+ 'Не удалось отправить закрепленное сообщение пользователю',
+ getattr=getattr(user, 'telegram_id', 'unknown'),
+ error=error,
)
@@ -141,7 +141,11 @@ async def _apply_campaign_bonus_if_needed(
async def handle_potential_referral_code(message: types.Message, state: FSMContext, db: AsyncSession):
current_state = await state.get_state()
- logger.info(f"🔍 REFERRAL/PROMO CHECK: Проверка сообщения '{message.text}' в состоянии {current_state}")
+ logger.info(
+ '🔍 REFERRAL/PROMO CHECK: Проверка сообщения в состоянии',
+ message_text=message.text,
+ current_state=current_state,
+ )
if current_state not in [
RegistrationStates.waiting_for_rules_accept.state,
@@ -188,7 +192,11 @@ async def handle_potential_referral_code(message: types.Message, state: FSMConte
await state.set_data(data)
await message.answer(texts.t('REFERRAL_CODE_ACCEPTED', '✅ Реферальный код принят!'))
- logger.info(f'✅ Реферальный код {potential_code} применен для пользователя {message.from_user.id}')
+ logger.info(
+ '✅ Реферальный код применен для пользователя',
+ potential_code=potential_code,
+ from_user_id=message.from_user.id,
+ )
if current_state != RegistrationStates.waiting_for_referral_code.state:
language = data.get('language', DEFAULT_LANGUAGE)
@@ -219,7 +227,11 @@ async def handle_potential_referral_code(message: types.Message, state: FSMConte
'✅ Промокод принят! Он будет активирован после завершения регистрации.',
)
)
- logger.info(f'✅ Промокод {potential_code} сохранен для активации для пользователя {message.from_user.id}')
+ logger.info(
+ '✅ Промокод сохранен для активации для пользователя',
+ potential_code=potential_code,
+ from_user_id=message.from_user.id,
+ )
if current_state != RegistrationStates.waiting_for_referral_code.state:
language = data.get('language', DEFAULT_LANGUAGE)
@@ -254,7 +266,7 @@ def _get_language_prompt_text() -> str:
async def _prompt_language_selection(message: types.Message, state: FSMContext) -> None:
- logger.info(f'🌐 LANGUAGE: Запрос выбора языка для пользователя {message.from_user.id}')
+ logger.info('🌐 LANGUAGE: Запрос выбора языка для пользователя', from_user_id=message.from_user.id)
await state.set_state(RegistrationStates.waiting_for_language)
await message.answer(
@@ -293,7 +305,7 @@ async def _continue_registration_after_language(
if referrer:
data['referrer_id'] = referrer.id
await state.set_data(data)
- logger.info(f'✅ LANGUAGE: Реферер найден: {referrer.id}')
+ logger.info('✅ LANGUAGE: Реферер найден', referrer_id=referrer.id)
if settings.SKIP_REFERRAL_CODE or data.get('referral_code'):
await _complete_registration_wrapper()
@@ -309,7 +321,7 @@ async def _continue_registration_after_language(
await state.set_state(RegistrationStates.waiting_for_referral_code)
logger.info('🔍 LANGUAGE: Ожидание ввода реферального кода')
except Exception as error:
- logger.error(f'Ошибка при показе вопроса о реферальном коде после выбора языка: {error}')
+ logger.error('Ошибка при показе вопроса о реферальном коде после выбора языка', error=error)
await _complete_registration_wrapper()
return
@@ -318,7 +330,8 @@ async def _continue_registration_after_language(
await target_message.answer(rules_text, reply_markup=get_rules_keyboard(language))
except TelegramForbiddenError:
logger.warning(
- f'⚠️ Пользователь {callback.from_user.id if callback else message.from_user.id} заблокировал бота, пропускаем отправку правил'
+ '⚠️ Пользователь заблокировал бота, пропускаем отправку правил',
+ from_user_id=callback.from_user.id if callback else message.from_user.id,
)
return
await state.set_state(RegistrationStates.waiting_for_rules_accept)
@@ -326,7 +339,7 @@ async def _continue_registration_after_language(
async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession, db_user=None):
- logger.info(f'🚀 START: Обработка /start от {message.from_user.id}')
+ logger.info('🚀 START: Обработка /start от', from_user_id=message.from_user.id)
data = await state.get_data() or {}
@@ -346,8 +359,7 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
data['pending_start_payload'] = redis_payload
state_needs_update = True
logger.info(
- "📦 START: Payload '%s' восстановлен из Redis (fallback)",
- pending_start_payload,
+ "📦 START: Payload '' восстановлен из Redis (fallback)", pending_start_payload=pending_start_payload
)
# НЕ удаляем Redis payload здесь - удаление только после успешной регистрации
@@ -360,10 +372,7 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
start_parameter = start_args[1]
elif pending_start_payload:
start_parameter = pending_start_payload
- logger.info(
- "📦 START: Используем сохраненный payload '%s'",
- pending_start_payload,
- )
+ logger.info("📦 START: Используем сохраненный payload ''", pending_start_payload=pending_start_payload)
if state_needs_update:
await state.set_data(data)
@@ -377,14 +386,14 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
if campaign:
logger.info(
- '📣 Найдена рекламная кампания %s (start=%s)',
- campaign.id,
- campaign.start_parameter,
+ '📣 Найдена рекламная кампания (start=)',
+ campaign_id=campaign.id,
+ start_parameter=campaign.start_parameter,
)
await state.update_data(campaign_id=campaign.id)
else:
referral_code = start_parameter
- logger.info(f'🔎 Найден реферальный код: {referral_code}')
+ logger.info('🔎 Найден реферальный код', referral_code=referral_code)
if referral_code:
await state.update_data(referral_code=referral_code)
@@ -402,32 +411,32 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
)
except Exception as notify_error:
logger.error(
- 'Ошибка отправки админ уведомления о переходе по кампании %s: %s',
- campaign.id,
- notify_error,
+ 'Ошибка отправки админ уведомления о переходе по кампании',
+ campaign_id=campaign.id,
+ notify_error=notify_error,
)
if user and user.status != UserStatus.DELETED.value:
- logger.info(f'✅ Активный пользователь найден: {user.telegram_id}')
+ logger.info('✅ Активный пользователь найден', telegram_id=user.telegram_id)
profile_updated = False
if user.username != message.from_user.username:
old_username = user.username
user.username = message.from_user.username
- logger.info(f"📝 Username обновлен: '{old_username}' → '{user.username}'")
+ logger.info('📝 Username обновлен', old_username=old_username, username=user.username)
profile_updated = True
if user.first_name != message.from_user.first_name:
old_first_name = user.first_name
user.first_name = message.from_user.first_name
- logger.info(f"📝 Имя обновлено: '{old_first_name}' → '{user.first_name}'")
+ logger.info('📝 Имя обновлено', old_first_name=old_first_name, first_name=user.first_name)
profile_updated = True
if user.last_name != message.from_user.last_name:
old_last_name = user.last_name
user.last_name = message.from_user.last_name
- logger.info(f"📝 Фамилия обновлена: '{old_last_name}' → '{user.last_name}'")
+ logger.info('📝 Фамилия обновлена', old_last_name=old_last_name, last_name=user.last_name)
profile_updated = True
user.last_activity = datetime.utcnow()
@@ -436,7 +445,7 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
- logger.info(f'💾 Профиль пользователя {user.telegram_id} обновлен')
+ logger.info('💾 Профиль пользователя обновлен', telegram_id=user.telegram_id)
else:
await db.commit()
@@ -459,7 +468,7 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
)
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления о рекламной кампании: {e}')
+ logger.error('Ошибка отправки уведомления о рекламной кампании', error=e)
has_active_subscription, subscription_is_active = _calculate_subscription_flags(user.subscription)
@@ -503,7 +512,7 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
return
if user and user.status == UserStatus.DELETED.value:
- logger.info(f'🔄 Удаленный пользователь {user.telegram_id} начинает повторную регистрацию')
+ logger.info('🔄 Удаленный пользователь начинает повторную регистрацию', telegram_id=user.telegram_id)
try:
from sqlalchemy import delete, update as sa_update
@@ -589,10 +598,10 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
await db.commit()
- logger.info(f'✅ Пользователь {user.telegram_id} подготовлен к восстановлению')
+ logger.info('✅ Пользователь подготовлен к восстановлению', telegram_id=user.telegram_id)
except Exception as e:
- logger.error(f'❌ Ошибка подготовки к восстановлению: {e}')
+ logger.error('❌ Ошибка подготовки к восстановлению', error=e)
await db.rollback()
else:
logger.info('🆕 Новый пользователь, начинаем регистрацию')
@@ -612,8 +621,8 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
data['language'] = normalized_default
await state.set_data(data)
logger.info(
- "🌐 LANGUAGE: выбор языка отключен, устанавливаем язык по умолчанию '%s'",
- normalized_default,
+ "🌐 LANGUAGE: выбор языка отключен, устанавливаем язык по умолчанию ''",
+ normalized_default=normalized_default,
)
await _continue_registration_after_language(
@@ -629,7 +638,9 @@ async def process_language_selection(
state: FSMContext,
db: AsyncSession,
):
- logger.info(f'🌐 LANGUAGE: Пользователь {callback.from_user.id} выбрал язык ({callback.data})')
+ logger.info(
+ '🌐 LANGUAGE: Пользователь выбрал язык', from_user_id=callback.from_user.id, callback_data=callback.data
+ )
if not settings.is_language_selection_enabled():
data = await state.get_data() or {}
@@ -680,7 +691,9 @@ async def process_language_selection(
if normalized_selected not in available_map:
logger.warning(
- f"⚠️ LANGUAGE: Выбран недоступный язык '{normalized_selected}' пользователем {callback.from_user.id}"
+ '⚠️ LANGUAGE: Выбран недоступный язык пользователем',
+ normalized_selected=normalized_selected,
+ from_user_id=callback.from_user.id,
)
await callback.answer('❌ Unsupported language', show_alert=True)
return
@@ -698,7 +711,7 @@ async def process_language_selection(
texts.t('LANGUAGE_SELECTED', '🌐 Язык интерфейса обновлен.'),
)
except Exception as error:
- logger.warning(f'⚠️ LANGUAGE: Не удалось обновить сообщение выбора языка: {error}')
+ logger.warning('⚠️ LANGUAGE: Не удалось обновить сообщение выбора языка', error=error)
await callback.message.answer(
texts.t('LANGUAGE_SELECTED', '🌐 Язык интерфейса обновлен.'),
)
@@ -734,27 +747,30 @@ async def _show_privacy_policy_after_rules(
if not privacy_policy_text or not privacy_policy_text.strip():
logger.info('⚠️ Политика конфиденциальности включена, но дефолтный текст пустой, пропускаем показ')
return False
- logger.info(f'🔒 Используется дефолтный текст политики конфиденциальности из локализации для языка {language}')
+ logger.info(
+ '🔒 Используется дефолтный текст политики конфиденциальности из локализации для языка', language=language
+ )
else:
privacy_policy_text = policy.content
- logger.info(f'🔒 Используется политика конфиденциальности из БД для языка {language}')
+ logger.info('🔒 Используется политика конфиденциальности из БД для языка', language=language)
try:
await callback.message.edit_text(privacy_policy_text, reply_markup=get_privacy_policy_keyboard(language))
await state.set_state(RegistrationStates.waiting_for_privacy_policy_accept)
- logger.info(f'🔒 Политика конфиденциальности отправлена пользователю {callback.from_user.id}')
+ logger.info('🔒 Политика конфиденциальности отправлена пользователю', from_user_id=callback.from_user.id)
return True
except Exception as e:
- logger.error(f'Ошибка при показе политики конфиденциальности: {e}', exc_info=True)
+ logger.error('Ошибка при показе политики конфиденциальности', error=e, exc_info=True)
try:
await callback.message.answer(privacy_policy_text, reply_markup=get_privacy_policy_keyboard(language))
await state.set_state(RegistrationStates.waiting_for_privacy_policy_accept)
logger.info(
- f'🔒 Политика конфиденциальности отправлена новым сообщением пользователю {callback.from_user.id}'
+ '🔒 Политика конфиденциальности отправлена новым сообщением пользователю',
+ from_user_id=callback.from_user.id,
)
return True
except Exception as e2:
- logger.error(f'Критическая ошибка при отправке политики конфиденциальности: {e2}', exc_info=True)
+ logger.error('Критическая ошибка при отправке политики конфиденциальности', e2=e2, exc_info=True)
return False
@@ -771,13 +787,13 @@ async def _continue_registration_after_rules(
texts = get_texts(language)
if data.get('referral_code'):
- logger.info(f'🎫 Найден реферальный код из deep link: {data["referral_code"]}')
+ logger.info('🎫 Найден реферальный код из deep link', data=data['referral_code'])
referrer = await get_user_by_referral_code(db, data['referral_code'])
if referrer:
data['referrer_id'] = referrer.id
await state.set_data(data)
- logger.info(f'✅ Реферер найден: {referrer.id}')
+ logger.info('✅ Реферер найден', referrer_id=referrer.id)
await complete_registration_from_callback(callback, state, db)
elif settings.SKIP_REFERRAL_CODE:
@@ -795,7 +811,7 @@ async def _continue_registration_after_rules(
await state.set_state(RegistrationStates.waiting_for_referral_code)
logger.info('🔍 Ожидание ввода реферального кода')
except Exception as e:
- logger.error(f'Ошибка при показе вопроса о реферальном коде: {e}')
+ logger.error('Ошибка при показе вопроса о реферальном коде', error=e)
await complete_registration_from_callback(callback, state, db)
@@ -804,11 +820,11 @@ async def process_rules_accept(callback: types.CallbackQuery, state: FSMContext,
Обрабатывает принятие или отклонение правил пользователем.
"""
logger.info('📋 RULES: Начало обработки правил')
- logger.info(f'📊 Callback data: {callback.data}')
- logger.info(f'👤 User: {callback.from_user.id}')
+ logger.info('📊 Callback data', callback_data=callback.data)
+ logger.info('👤 User', from_user_id=callback.from_user.id)
current_state = await state.get_state()
- logger.info(f'📊 Текущее состояние: {current_state}')
+ logger.info('📊 Текущее состояние', current_state=current_state)
language = DEFAULT_LANGUAGE
texts = get_texts(language)
@@ -821,7 +837,7 @@ async def process_rules_accept(callback: types.CallbackQuery, state: FSMContext,
texts = get_texts(language)
if callback.data == 'rules_accept':
- logger.info(f'✅ Правила приняты пользователем {callback.from_user.id}')
+ logger.info('✅ Правила приняты пользователем', from_user_id=callback.from_user.id)
# Пытаемся показать политику конфиденциальности
policy_shown = await _show_privacy_policy_after_rules(callback, state, db, language)
@@ -831,7 +847,7 @@ async def process_rules_accept(callback: types.CallbackQuery, state: FSMContext,
await _continue_registration_after_rules(callback, state, db, language)
else:
- logger.info(f'❌ Правила отклонены пользователем {callback.from_user.id}')
+ logger.info('❌ Правила отклонены пользователем', from_user_id=callback.from_user.id)
rules_required_text = texts.t(
'RULES_REQUIRED',
@@ -844,12 +860,12 @@ async def process_rules_accept(callback: types.CallbackQuery, state: FSMContext,
if 'message is not modified' in str(e):
pass # Сообщение уже содержит нужный текст
else:
- logger.error(f'Ошибка при показе сообщения об отклонении правил: {e}')
+ logger.error('Ошибка при показе сообщения об отклонении правил', error=e)
- logger.info(f'✅ Правила обработаны для пользователя {callback.from_user.id}')
+ logger.info('✅ Правила обработаны для пользователя', from_user_id=callback.from_user.id)
except Exception as e:
- logger.error(f'❌ Ошибка обработки правил: {e}', exc_info=True)
+ logger.error('❌ Ошибка обработки правил', error=e, exc_info=True)
await callback.answer(
texts.t('ERROR_TRY_AGAIN', '❌ Произошла ошибка. Попробуйте еще раз.'),
show_alert=True,
@@ -873,11 +889,11 @@ async def process_rules_accept(callback: types.CallbackQuery, state: FSMContext,
async def process_privacy_policy_accept(callback: types.CallbackQuery, state: FSMContext, db: AsyncSession):
logger.info('🔒 PRIVACY POLICY: Начало обработки политики конфиденциальности')
- logger.info(f'📊 Callback data: {callback.data}')
- logger.info(f'👤 User: {callback.from_user.id}')
+ logger.info('📊 Callback data', callback_data=callback.data)
+ logger.info('👤 User', from_user_id=callback.from_user.id)
current_state = await state.get_state()
- logger.info(f'📊 Текущее состояние: {current_state}')
+ logger.info('📊 Текущее состояние', current_state=current_state)
language = DEFAULT_LANGUAGE
texts = get_texts(language)
@@ -890,13 +906,13 @@ async def process_privacy_policy_accept(callback: types.CallbackQuery, state: FS
texts = get_texts(language)
if callback.data == 'privacy_policy_accept':
- logger.info(f'✅ Политика конфиденциальности принята пользователем {callback.from_user.id}')
+ logger.info('✅ Политика конфиденциальности принята пользователем', from_user_id=callback.from_user.id)
try:
await callback.message.delete()
logger.info('🗑️ Сообщение с политикой конфиденциальности удалено')
except Exception as e:
- logger.warning(f'⚠️ Не удалось удалить сообщение с политикой конфиденциальности: {e}')
+ logger.warning('⚠️ Не удалось удалить сообщение с политикой конфиденциальности', error=e)
try:
await callback.message.edit_text(
texts.t(
@@ -909,13 +925,13 @@ async def process_privacy_policy_accept(callback: types.CallbackQuery, state: FS
pass
if data.get('referral_code'):
- logger.info(f'🎫 Найден реферальный код из deep link: {data["referral_code"]}')
+ logger.info('🎫 Найден реферальный код из deep link', data=data['referral_code'])
referrer = await get_user_by_referral_code(db, data['referral_code'])
if referrer:
data['referrer_id'] = referrer.id
await state.set_data(data)
- logger.info(f'✅ Реферер найден: {referrer.id}')
+ logger.info('✅ Реферер найден', referrer_id=referrer.id)
await complete_registration_from_callback(callback, state, db)
elif settings.SKIP_REFERRAL_CODE:
@@ -936,11 +952,11 @@ async def process_privacy_policy_accept(callback: types.CallbackQuery, state: FS
)
logger.info('🔍 Ожидание ввода реферального кода')
except Exception as e:
- logger.error(f'Ошибка при показе вопроса о реферальном коде: {e}')
+ logger.error('Ошибка при показе вопроса о реферальном коде', error=e)
await complete_registration_from_callback(callback, state, db)
else:
- logger.info(f'❌ Политика конфиденциальности отклонена пользователем {callback.from_user.id}')
+ logger.info('❌ Политика конфиденциальности отклонена пользователем', from_user_id=callback.from_user.id)
privacy_policy_required_text = texts.t(
'PRIVACY_POLICY_REQUIRED',
@@ -953,14 +969,14 @@ async def process_privacy_policy_accept(callback: types.CallbackQuery, state: FS
)
except TelegramBadRequest as e:
if 'message is not modified' not in str(e):
- logger.warning(f'Ошибка при показе сообщения об отклонении политики: {e}')
+ logger.warning('Ошибка при показе сообщения об отклонении политики', error=e)
except Exception as e:
- logger.warning(f'Ошибка при показе сообщения об отклонении политики: {e}')
+ logger.warning('Ошибка при показе сообщения об отклонении политики', error=e)
- logger.info(f'✅ Политика конфиденциальности обработана для пользователя {callback.from_user.id}')
+ logger.info('✅ Политика конфиденциальности обработана для пользователя', from_user_id=callback.from_user.id)
except Exception as e:
- logger.error(f'❌ Ошибка обработки политики конфиденциальности: {e}', exc_info=True)
+ logger.error('❌ Ошибка обработки политики конфиденциальности', error=e, exc_info=True)
await callback.answer(
texts.t('ERROR_TRY_AGAIN', '❌ Произошла ошибка. Попробуйте еще раз.'),
show_alert=True,
@@ -983,7 +999,7 @@ async def process_privacy_policy_accept(callback: types.CallbackQuery, state: FS
async def process_referral_code_input(message: types.Message, state: FSMContext, db: AsyncSession):
- logger.info(f'🎫 REFERRAL/PROMO: Обработка кода: {message.text}')
+ logger.info('🎫 REFERRAL/PROMO: Обработка кода', message_text=message.text)
data = await state.get_data() or {}
language = data.get('language', DEFAULT_LANGUAGE)
@@ -1015,7 +1031,7 @@ async def process_referral_code_input(message: types.Message, state: FSMContext,
data['referrer_id'] = referrer.id
await state.set_data(data)
await message.answer(texts.t('REFERRAL_CODE_ACCEPTED', '✅ Реферальный код принят!'))
- logger.info(f'✅ Реферальный код применен: {code}')
+ logger.info('✅ Реферальный код применен', code=code)
await complete_registration(message, state, db)
return
@@ -1034,7 +1050,7 @@ async def process_referral_code_input(message: types.Message, state: FSMContext,
'✅ Промокод принят! Он будет активирован после завершения регистрации.',
)
)
- logger.info(f'✅ Промокод сохранен для активации: {code}')
+ logger.info('✅ Промокод сохранен для активации', code=code)
await complete_registration(message, state, db)
return
@@ -1043,12 +1059,12 @@ async def process_referral_code_input(message: types.Message, state: FSMContext,
promo_limiter.cleanup()
await message.answer(texts.t('REFERRAL_OR_PROMO_CODE_INVALID', '❌ Неверный реферальный код или промокод'))
- logger.info(f'❌ Неверный код (ни реферальный, ни промокод): {code}')
+ logger.info('❌ Неверный код (ни реферальный, ни промокод)', code=code)
return
async def process_referral_code_skip(callback: types.CallbackQuery, state: FSMContext, db: AsyncSession):
- logger.info(f'⭐️ SKIP: Пропуск реферального кода от пользователя {callback.from_user.id}')
+ logger.info('⭐️ SKIP: Пропуск реферального кода от пользователя', from_user_id=callback.from_user.id)
await callback.answer()
data = await state.get_data() or {}
@@ -1059,7 +1075,7 @@ async def process_referral_code_skip(callback: types.CallbackQuery, state: FSMCo
await callback.message.delete()
logger.info('🗑️ Сообщение с вопросом о реферальном коде удалено')
except Exception as e:
- logger.warning(f'⚠️ Не удалось удалить сообщение с вопросом о реферальном коде: {e}')
+ logger.warning('⚠️ Не удалось удалить сообщение с вопросом о реферальном коде', error=e)
try:
await callback.message.edit_text(
texts.t('REGISTRATION_COMPLETING', '✅ Завершаем регистрацию...'), reply_markup=None
@@ -1071,12 +1087,12 @@ async def process_referral_code_skip(callback: types.CallbackQuery, state: FSMCo
async def complete_registration_from_callback(callback: types.CallbackQuery, state: FSMContext, db: AsyncSession):
- logger.info(f'🎯 COMPLETE: Завершение регистрации для пользователя {callback.from_user.id}')
+ logger.info('🎯 COMPLETE: Завершение регистрации для пользователя', from_user_id=callback.from_user.id)
existing_user = await get_user_by_telegram_id(db, callback.from_user.id)
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
- logger.warning(f'⚠️ Пользователь {callback.from_user.id} уже активен! Показываем главное меню.')
+ logger.warning('⚠️ Пользователь уже активен! Показываем главное меню.', from_user_id=callback.from_user.id)
texts = get_texts(existing_user.language)
data = await state.get_data() or {}
@@ -1123,7 +1139,7 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
await callback.message.answer(menu_text, reply_markup=keyboard, parse_mode='HTML')
await _send_pinned_message(callback.bot, db, existing_user)
except Exception as e:
- logger.error(f'Ошибка при показе главного меню существующему пользователю: {e}')
+ logger.error('Ошибка при показе главного меню существующему пользователю', error=e)
await callback.message.answer(
texts.t(
'WELCOME_FALLBACK',
@@ -1150,7 +1166,7 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
referrer_id = referrer.id
if existing_user and existing_user.status == UserStatus.DELETED.value:
- logger.info(f'🔄 Восстанавливаем удаленного пользователя {callback.from_user.id}')
+ logger.info('🔄 Восстанавливаем удаленного пользователя', from_user_id=callback.from_user.id)
existing_user.username = callback.from_user.username
existing_user.first_name = callback.from_user.first_name
@@ -1170,10 +1186,10 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
await db.refresh(existing_user, ['subscription'])
user = existing_user
- logger.info(f'✅ Пользователь {callback.from_user.id} восстановлен')
+ logger.info('✅ Пользователь восстановлен', from_user_id=callback.from_user.id)
elif not existing_user:
- logger.info(f'🆕 Создаем нового пользователя {callback.from_user.id}')
+ logger.info('🆕 Создаем нового пользователя', from_user_id=callback.from_user.id)
referral_code = await generate_unique_referral_code(db, callback.from_user.id)
@@ -1189,7 +1205,7 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
)
await db.refresh(user, ['subscription'])
else:
- logger.info(f'🔄 Обновляем существующего пользователя {callback.from_user.id}')
+ logger.info('🔄 Обновляем существующего пользователя', from_user_id=callback.from_user.id)
existing_user.status = UserStatus.ACTIVE.value
existing_user.language = language
if referrer_id and not existing_user.referred_by_id:
@@ -1207,9 +1223,9 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
if referrer_id:
try:
await process_referral_registration(db, user.id, referrer_id, callback.bot)
- logger.info(f'✅ Реферальная регистрация обработана для {user.id}')
+ logger.info('✅ Реферальная регистрация обработана для', user_id=user.id)
except Exception as e:
- logger.error(f'Ошибка при обработке реферальной регистрации: {e}')
+ logger.error('Ошибка при обработке реферальной регистрации', error=e)
campaign_message = await _apply_campaign_bonus_if_needed(db, user, data, texts)
@@ -1217,24 +1233,25 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
await db.refresh(user)
except Exception as refresh_error:
logger.error(
- 'Ошибка обновления данных пользователя %s после бонуса кампании: %s',
- user.telegram_id,
- refresh_error,
+ 'Ошибка обновления данных пользователя после бонуса кампании',
+ telegram_id=user.telegram_id,
+ refresh_error=refresh_error,
)
try:
await db.refresh(user, ['subscription'])
except Exception as refresh_subscription_error:
logger.error(
- 'Ошибка обновления подписки пользователя %s после бонуса кампании: %s',
- user.telegram_id,
- refresh_subscription_error,
+ 'Ошибка обновления подписки пользователя после бонуса кампании',
+ telegram_id=user.telegram_id,
+ refresh_subscription_error=refresh_subscription_error,
)
# ИСПРАВЛЕНИЕ БАГА: Очищаем Redis payload после успешной регистрации
await delete_pending_payload_from_redis(callback.from_user.id)
logger.info(
- '🗑️ COMPLETE_FROM_CALLBACK: Redis payload удален после успешной регистрации пользователя %s', user.telegram_id
+ '🗑️ COMPLETE_FROM_CALLBACK: Redis payload удален после успешной регистрации пользователя',
+ telegram_id=user.telegram_id,
)
await state.clear()
@@ -1243,7 +1260,7 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
try:
await callback.message.answer(campaign_message)
except Exception as e:
- logger.error(f'Ошибка отправки сообщения о бонусе кампании: {e}')
+ logger.error('Ошибка отправки сообщения о бонусе кампании', error=e)
from app.database.crud.welcome_text import get_welcome_text_for_user
@@ -1253,9 +1270,9 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
if skip_welcome_offer:
logger.info(
- 'ℹ️ Пропускаем приветственное предложение для нового пользователя %s из рекламной кампании %s',
- user.telegram_id,
- campaign_id,
+ 'ℹ️ Пропускаем приветственное предложение для нового пользователя из рекламной кампании',
+ telegram_id=user.telegram_id,
+ campaign_id=campaign_id,
)
if offer_text and not skip_welcome_offer:
@@ -1264,11 +1281,11 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
offer_text,
reply_markup=get_post_registration_keyboard(user.language),
)
- logger.info(f'✅ Приветственное сообщение отправлено пользователю {user.telegram_id}')
+ logger.info('✅ Приветственное сообщение отправлено пользователю', telegram_id=user.telegram_id)
await _send_pinned_message(callback.bot, db, user)
except TelegramBadRequest as e:
if 'parse entities' in str(e).lower() or "can't parse" in str(e).lower():
- logger.warning(f'HTML parse error в приветственном сообщении, повтор без parse_mode: {e}')
+ logger.warning('HTML parse error в приветственном сообщении, повтор без parse_mode', error=e)
try:
await callback.message.answer(
offer_text,
@@ -1277,14 +1294,15 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
)
await _send_pinned_message(callback.bot, db, user)
except Exception as fallback_err:
- logger.error(f'Ошибка при повторной отправке приветственного сообщения: {fallback_err}')
+ logger.error('Ошибка при повторной отправке приветственного сообщения', fallback_err=fallback_err)
else:
- logger.error(f'Ошибка при отправке приветственного сообщения: {e}')
+ logger.error('Ошибка при отправке приветственного сообщения', error=e)
except Exception as e:
- logger.error(f'Ошибка при отправке приветственного сообщения: {e}')
+ logger.error('Ошибка при отправке приветственного сообщения', error=e)
else:
logger.info(
- f'ℹ️ Приветственные сообщения отключены, показываем главное меню для пользователя {user.telegram_id}'
+ 'ℹ️ Приветственные сообщения отключены, показываем главное меню для пользователя',
+ telegram_id=user.telegram_id,
)
has_active_subscription, subscription_is_active = _calculate_subscription_flags(
@@ -1321,9 +1339,9 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
)
await callback.message.answer(menu_text, reply_markup=keyboard, parse_mode='HTML')
await _send_pinned_message(callback.bot, db, user)
- logger.info(f'✅ Главное меню показано пользователю {user.telegram_id}')
+ logger.info('✅ Главное меню показано пользователю', telegram_id=user.telegram_id)
except Exception as e:
- logger.error(f'Ошибка при показе главного меню: {e}')
+ logger.error('Ошибка при показе главного меню', error=e)
await callback.message.answer(
texts.t(
'WELCOME_FALLBACK',
@@ -1331,16 +1349,16 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
).format(user_name=user.full_name)
)
- logger.info(f'✅ Регистрация завершена для пользователя: {user.telegram_id}')
+ logger.info('✅ Регистрация завершена для пользователя', telegram_id=user.telegram_id)
async def complete_registration(message: types.Message, state: FSMContext, db: AsyncSession):
- logger.info(f'🎯 COMPLETE: Завершение регистрации для пользователя {message.from_user.id}')
+ logger.info('🎯 COMPLETE: Завершение регистрации для пользователя', from_user_id=message.from_user.id)
existing_user = await get_user_by_telegram_id(db, message.from_user.id)
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
- logger.warning(f'⚠️ Пользователь {message.from_user.id} уже активен! Показываем главное меню.')
+ logger.warning('⚠️ Пользователь уже активен! Показываем главное меню.', from_user_id=message.from_user.id)
texts = get_texts(existing_user.language)
data = await state.get_data() or {}
@@ -1387,7 +1405,7 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
await message.answer(menu_text, reply_markup=keyboard, parse_mode='HTML')
await _send_pinned_message(message.bot, db, existing_user)
except Exception as e:
- logger.error(f'Ошибка при показе главного меню существующему пользователю: {e}')
+ logger.error('Ошибка при показе главного меню существующему пользователю', error=e)
await message.answer(
texts.t(
'WELCOME_FALLBACK',
@@ -1414,7 +1432,7 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
referrer_id = referrer.id
if existing_user and existing_user.status == UserStatus.DELETED.value:
- logger.info(f'🔄 Восстанавливаем удаленного пользователя {message.from_user.id}')
+ logger.info('🔄 Восстанавливаем удаленного пользователя', from_user_id=message.from_user.id)
existing_user.username = message.from_user.username
existing_user.first_name = message.from_user.first_name
@@ -1434,10 +1452,10 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
await db.refresh(existing_user, ['subscription'])
user = existing_user
- logger.info(f'✅ Пользователь {message.from_user.id} восстановлен')
+ logger.info('✅ Пользователь восстановлен', from_user_id=message.from_user.id)
elif not existing_user:
- logger.info(f'🆕 Создаем нового пользователя {message.from_user.id}')
+ logger.info('🆕 Создаем нового пользователя', from_user_id=message.from_user.id)
referral_code = await generate_unique_referral_code(db, message.from_user.id)
@@ -1453,7 +1471,7 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
)
await db.refresh(user, ['subscription'])
else:
- logger.info(f'🔄 Обновляем существующего пользователя {message.from_user.id}')
+ logger.info('🔄 Обновляем существующего пользователя', from_user_id=message.from_user.id)
existing_user.status = UserStatus.ACTIVE.value
existing_user.language = language
if referrer_id and not existing_user.referred_by_id:
@@ -1471,9 +1489,9 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
if referrer_id:
try:
await process_referral_registration(db, user.id, referrer_id, message.bot)
- logger.info(f'✅ Реферальная регистрация обработана для {user.id}')
+ logger.info('✅ Реферальная регистрация обработана для', user_id=user.id)
except Exception as e:
- logger.error(f'Ошибка при обработке реферальной регистрации: {e}')
+ logger.error('Ошибка при обработке реферальной регистрации', error=e)
# Активируем промокод если был сохранен в state
promocode_to_activate = data.get('promocode')
@@ -1491,13 +1509,19 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
description=promocode_result['description']
)
)
- logger.info(f'✅ Промокод {promocode_to_activate} активирован для пользователя {user.id}')
+ logger.info(
+ '✅ Промокод активирован для пользователя',
+ promocode_to_activate=promocode_to_activate,
+ user_id=user.id,
+ )
else:
logger.warning(
- f'⚠️ Не удалось активировать промокод {promocode_to_activate}: {promocode_result.get("error")}'
+ '⚠️ Не удалось активировать промокод',
+ promocode_to_activate=promocode_to_activate,
+ get=promocode_result.get('error'),
)
except Exception as e:
- logger.error(f'❌ Ошибка при активации промокода {promocode_to_activate}: {e}')
+ logger.error('❌ Ошибка при активации промокода', promocode_to_activate=promocode_to_activate, error=e)
campaign_message = await _apply_campaign_bonus_if_needed(db, user, data, texts)
@@ -1505,23 +1529,25 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
await db.refresh(user)
except Exception as refresh_error:
logger.error(
- 'Ошибка обновления данных пользователя %s после бонуса кампании: %s',
- user.telegram_id,
- refresh_error,
+ 'Ошибка обновления данных пользователя после бонуса кампании',
+ telegram_id=user.telegram_id,
+ refresh_error=refresh_error,
)
try:
await db.refresh(user, ['subscription'])
except Exception as refresh_subscription_error:
logger.error(
- 'Ошибка обновления подписки пользователя %s после бонуса кампании: %s',
- user.telegram_id,
- refresh_subscription_error,
+ 'Ошибка обновления подписки пользователя после бонуса кампании',
+ telegram_id=user.telegram_id,
+ refresh_subscription_error=refresh_subscription_error,
)
# ИСПРАВЛЕНИЕ БАГА: Очищаем Redis payload после успешной регистрации
await delete_pending_payload_from_redis(message.from_user.id)
- logger.info('🗑️ COMPLETE: Redis payload удален после успешной регистрации пользователя %s', user.telegram_id)
+ logger.info(
+ '🗑️ COMPLETE: Redis payload удален после успешной регистрации пользователя', telegram_id=user.telegram_id
+ )
await state.clear()
@@ -1529,7 +1555,7 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
try:
await message.answer(campaign_message)
except Exception as e:
- logger.error(f'Ошибка отправки сообщения о бонусе кампании: {e}')
+ logger.error('Ошибка отправки сообщения о бонусе кампании', error=e)
from app.database.crud.welcome_text import get_welcome_text_for_user
@@ -1539,9 +1565,9 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
if skip_welcome_offer:
logger.info(
- 'ℹ️ Пропускаем приветственное предложение для нового пользователя %s из рекламной кампании %s',
- user.telegram_id,
- campaign_id,
+ 'ℹ️ Пропускаем приветственное предложение для нового пользователя из рекламной кампании',
+ telegram_id=user.telegram_id,
+ campaign_id=campaign_id,
)
if offer_text and not skip_welcome_offer:
@@ -1557,11 +1583,11 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
offer_text,
reply_markup=keyboard,
)
- logger.info(f'✅ Приветственное сообщение отправлено пользователю {user.telegram_id}')
+ logger.info('✅ Приветственное сообщение отправлено пользователю', telegram_id=user.telegram_id)
await _send_pinned_message(message.bot, db, user)
except TelegramBadRequest as e:
if 'parse entities' in str(e).lower() or "can't parse" in str(e).lower():
- logger.warning(f'HTML parse error в приветственном сообщении, повтор без parse_mode: {e}')
+ logger.warning('HTML parse error в приветственном сообщении, повтор без parse_mode', error=e)
try:
await message.answer(
offer_text,
@@ -1570,14 +1596,15 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
)
await _send_pinned_message(message.bot, db, user)
except Exception as fallback_err:
- logger.error(f'Ошибка при повторной отправке приветственного сообщения: {fallback_err}')
+ logger.error('Ошибка при повторной отправке приветственного сообщения', fallback_err=fallback_err)
else:
- logger.error(f'Ошибка при отправке приветственного сообщения: {e}')
+ logger.error('Ошибка при отправке приветственного сообщения', error=e)
except Exception as e:
- logger.error(f'Ошибка при отправке приветственного сообщения: {e}')
+ logger.error('Ошибка при отправке приветственного сообщения', error=e)
else:
logger.info(
- f'ℹ️ Приветственные сообщения отключены, показываем главное меню для пользователя {user.telegram_id}'
+ 'ℹ️ Приветственные сообщения отключены, показываем главное меню для пользователя',
+ telegram_id=user.telegram_id,
)
has_active_subscription, subscription_is_active = _calculate_subscription_flags(
@@ -1613,10 +1640,10 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
custom_buttons=custom_buttons,
)
await message.answer(menu_text, reply_markup=keyboard, parse_mode='HTML')
- logger.info(f'✅ Главное меню показано пользователю {user.telegram_id}')
+ logger.info('✅ Главное меню показано пользователю', telegram_id=user.telegram_id)
await _send_pinned_message(message.bot, db, user)
except Exception as e:
- logger.error(f'Ошибка при показе главного меню: {e}')
+ logger.error('Ошибка при показе главного меню', error=e)
await message.answer(
texts.t(
'WELCOME_FALLBACK',
@@ -1624,7 +1651,7 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
).format(user_name=user.full_name)
)
- logger.info(f'✅ Регистрация завершена для пользователя: {user.telegram_id}')
+ logger.info('✅ Регистрация завершена для пользователя', telegram_id=user.telegram_id)
def _get_subscription_status(user, texts):
@@ -1746,9 +1773,9 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
info_sections.append(promo_hint.strip())
except Exception as hint_error:
logger.debug(
- 'Не удалось построить подсказку промо-предложения для пользователя %s: %s',
- getattr(user, 'id', None),
- hint_error,
+ 'Не удалось построить подсказку промо-предложения для пользователя',
+ getattr=getattr(user, 'id', None),
+ hint_error=hint_error,
)
try:
@@ -1757,9 +1784,9 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
info_sections.append(test_access_hint.strip())
except Exception as test_error:
logger.debug(
- 'Не удалось построить подсказку тестового доступа для пользователя %s: %s',
- getattr(user, 'id', None),
- test_error,
+ 'Не удалось построить подсказку тестового доступа для пользователя',
+ getattr=getattr(user, 'id', None),
+ test_error=test_error,
)
if info_sections:
@@ -1773,7 +1800,7 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
return _insert_random_message(base_text, random_message, action_prompt)
except Exception as e:
- logger.error(f'Ошибка получения случайного сообщения: {e}')
+ logger.error('Ошибка получения случайного сообщения', error=e)
return base_text
@@ -1793,7 +1820,7 @@ async def get_main_menu_text_simple(user_name, texts, db: AsyncSession):
return _insert_random_message(base_text, random_message, action_prompt)
except Exception as e:
- logger.error(f'Ошибка получения случайного сообщения: {e}')
+ logger.error('Ошибка получения случайного сообщения', error=e)
return base_text
@@ -1819,15 +1846,12 @@ async def required_sub_channel_check(
pending_start_payload = redis_payload
state_data['pending_start_payload'] = redis_payload
logger.info(
- "📦 CHANNEL CHECK: Payload '%s' восстановлен из Redis (fallback)",
- pending_start_payload,
+ "📦 CHANNEL CHECK: Payload '' восстановлен из Redis (fallback)",
+ pending_start_payload=pending_start_payload,
)
if pending_start_payload:
- logger.info(
- "📦 CHANNEL CHECK: Найден сохраненный payload '%s'",
- pending_start_payload,
- )
+ logger.info("📦 CHANNEL CHECK: Найден сохраненный payload ''", pending_start_payload=pending_start_payload)
user = db_user
if not user:
@@ -1849,8 +1873,8 @@ async def required_sub_channel_check(
]:
# НЕ удаляем payload - пользователь может попробовать снова после подписки
logger.info(
- "📦 CHANNEL CHECK: Подписка не подтверждена, payload '%s' сохранён для следующей попытки",
- pending_start_payload,
+ "📦 CHANNEL CHECK: Подписка не подтверждена, payload '' сохранён для следующей попытки",
+ pending_start_payload=pending_start_payload,
)
return await query.answer(
texts.t('CHANNEL_SUBSCRIBE_REQUIRED_ALERT', '❌ Вы не подписались на канал!'),
@@ -1884,20 +1908,17 @@ async def required_sub_channel_check(
if campaign:
state_data['campaign_id'] = campaign.id
- logger.info(
- '📣 CHANNEL CHECK: Кампания %s восстановлена из payload',
- campaign.id,
- )
+ logger.info('📣 CHANNEL CHECK: Кампания восстановлена из payload', campaign_id=campaign.id)
else:
state_data['referral_code'] = pending_start_payload
logger.info(
- '🎯 CHANNEL CHECK: Payload интерпретирован как реферальный код: %s',
- pending_start_payload,
+ '🎯 CHANNEL CHECK: Payload интерпретирован как реферальный код',
+ pending_start_payload=pending_start_payload,
)
else:
logger.info(
- '✅ CHANNEL CHECK: Реферальный код уже сохранен в state: %s',
- state_data.get('referral_code') or f'campaign_id={state_data.get("campaign_id")}',
+ '✅ CHANNEL CHECK: Реферальный код уже сохранен в state',
+ state_data=state_data.get('referral_code') or f'campaign_id={state_data.get("campaign_id")}',
)
await state.set_data(state_data)
@@ -1910,8 +1931,8 @@ async def required_sub_channel_check(
await db.commit()
await db.refresh(subscription)
logger.info(
- '✅ Триальная подписка пользователя %s восстановлена после подтверждения подписки на канал',
- user.telegram_id,
+ '✅ Триальная подписка пользователя восстановлена после подтверждения подписки на канал',
+ telegram_id=user.telegram_id,
)
try:
@@ -1922,9 +1943,9 @@ async def required_sub_channel_check(
await subscription_service.create_remnawave_user(db, subscription)
except Exception as api_error:
logger.error(
- '❌ Ошибка обновления RemnaWave при восстановлении подписки пользователя %s: %s',
- user.telegram_id if user else query.from_user.id,
- api_error,
+ '❌ Ошибка обновления RemnaWave при восстановлении подписки пользователя',
+ telegram_id=user.telegram_id if user else query.from_user.id,
+ api_error=api_error,
)
await query.answer(
@@ -1935,7 +1956,7 @@ async def required_sub_channel_check(
try:
await query.message.delete()
except Exception as e:
- logger.warning(f'Не удалось удалить сообщение: {e}')
+ logger.warning('Не удалось удалить сообщение', error=e)
# ИСПРАВЛЕНИЕ БАГА: Очищаем Redis payload ТОЛЬКО после успешной проверки подписки
# и перед показом главного меню или завершением регистрации
@@ -2006,7 +2027,7 @@ async def required_sub_channel_check(
referrer = await get_user_by_referral_code(db, ref_code_from_link)
if referrer:
referrer_id = referrer.id
- logger.info(f'✅ CHANNEL CHECK: Реферер найден из ссылки: {referrer.id}')
+ logger.info('✅ CHANNEL CHECK: Реферер найден из ссылки', referrer_id=referrer.id)
referral_code = await generate_unique_referral_code(db, query.from_user.id)
@@ -2031,9 +2052,9 @@ async def required_sub_channel_check(
if referrer_id:
try:
await process_referral_registration(db, user.id, referrer_id, bot)
- logger.info(f'✅ CHANNEL CHECK: Реферальная регистрация обработана для {user.id}')
+ logger.info('✅ CHANNEL CHECK: Реферальная регистрация обработана для', user_id=user.id)
except Exception as e:
- logger.error(f'Ошибка при обработке реферальной регистрации: {e}')
+ logger.error('Ошибка при обработке реферальной регистрации', error=e)
# Показываем главное меню после создания пользователя
has_active_subscription, subscription_is_active = _calculate_subscription_flags(user.subscription)
@@ -2115,13 +2136,13 @@ async def required_sub_channel_check(
if 'query is too old' in error_msg or 'query id is invalid' in error_msg:
logger.debug('Устаревший callback в required_sub_channel_check, игнорируем')
else:
- logger.error(f'Ошибка Telegram API в required_sub_channel_check: {e}')
+ logger.error('Ошибка Telegram API в required_sub_channel_check', error=e)
try:
await query.answer(f'{texts.ERROR}!', show_alert=True)
except Exception:
pass
except Exception as e:
- logger.error(f'Ошибка в required_sub_channel_check: {e}')
+ logger.error('Ошибка в required_sub_channel_check', error=e)
try:
await query.answer(f'{texts.ERROR}!', show_alert=True)
except Exception:
@@ -2129,47 +2150,47 @@ async def required_sub_channel_check(
def register_handlers(dp: Dispatcher):
- logger.info('🔧 === НАЧАЛО регистрации обработчиков start.py ===')
+ logger.debug('=== НАЧАЛО регистрации обработчиков start.py ===')
dp.message.register(cmd_start, Command('start'))
- logger.info('✅ Зарегистрирован cmd_start')
+ logger.debug('Зарегистрирован cmd_start')
dp.callback_query.register(
process_rules_accept,
F.data.in_(['rules_accept', 'rules_decline']),
StateFilter(RegistrationStates.waiting_for_rules_accept),
)
- logger.info('✅ Зарегистрирован process_rules_accept')
+ logger.debug('Зарегистрирован process_rules_accept')
dp.callback_query.register(
process_privacy_policy_accept,
F.data.in_(['privacy_policy_accept', 'privacy_policy_decline']),
StateFilter(RegistrationStates.waiting_for_privacy_policy_accept),
)
- logger.info('✅ Зарегистрирован process_privacy_policy_accept')
+ logger.debug('Зарегистрирован process_privacy_policy_accept')
dp.callback_query.register(
process_language_selection,
F.data.startswith('language_select:'),
StateFilter(RegistrationStates.waiting_for_language),
)
- logger.info('✅ Зарегистрирован process_language_selection')
+ logger.debug('Зарегистрирован process_language_selection')
dp.callback_query.register(
process_referral_code_skip, F.data == 'referral_skip', StateFilter(RegistrationStates.waiting_for_referral_code)
)
- logger.info('✅ Зарегистрирован process_referral_code_skip')
+ logger.debug('Зарегистрирован process_referral_code_skip')
dp.message.register(process_referral_code_input, StateFilter(RegistrationStates.waiting_for_referral_code))
- logger.info('✅ Зарегистрирован process_referral_code_input')
+ logger.debug('Зарегистрирован process_referral_code_input')
dp.message.register(
handle_potential_referral_code,
StateFilter(RegistrationStates.waiting_for_rules_accept, RegistrationStates.waiting_for_referral_code),
)
- logger.info('✅ Зарегистрирован handle_potential_referral_code')
+ logger.debug('Зарегистрирован handle_potential_referral_code')
dp.callback_query.register(required_sub_channel_check, F.data.in_(['sub_channel_check']))
- logger.info('✅ Зарегистрирован required_sub_channel_check')
+ logger.debug('Зарегистрирован required_sub_channel_check')
- logger.info('🔧 === КОНЕЦ регистрации обработчиков start.py ===')
+ logger.debug('=== КОНЕЦ регистрации обработчиков start.py ===')
diff --git a/app/handlers/subscription/common.py b/app/handlers/subscription/common.py
index c5800be0..7dd99d68 100644
--- a/app/handlers/subscription/common.py
+++ b/app/handlers/subscription/common.py
@@ -1,10 +1,10 @@
import base64
import json
-import logging
from datetime import datetime
from typing import Any
from urllib.parse import quote
+import structlog
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from app.config import get_traffic_prices, settings
@@ -19,7 +19,7 @@ from app.utils.promo_offer import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
TRAFFIC_PRICES = get_traffic_prices()
@@ -39,7 +39,7 @@ def _format_text_with_placeholders(template: str, values: dict[str, Any]) -> str
try:
return template.format_map(safe_values)
except Exception: # pragma: no cover - defensive logging
- logger.warning("Failed to format template '%s' with values %s", template, values)
+ logger.warning("Failed to format template '' with values", template=template, values=values)
return template
@@ -164,7 +164,7 @@ def load_app_config() -> dict[str, Any]:
return data
logger.error('Некорректный формат app-config.json: ожидается объект')
except Exception as e:
- logger.error(f'Ошибка загрузки конфига приложений: {e}')
+ logger.error('Ошибка загрузки конфига приложений', error=e)
return {}
@@ -317,9 +317,7 @@ def create_deep_link(app: dict[str, Any], subscription_url: str) -> str | None:
payload = base64.b64encode(subscription_url.encode('utf-8')).decode('utf-8')
except Exception as exc:
logger.warning(
- 'Не удалось закодировать ссылку подписки в base64 для приложения %s: %s',
- app.get('id'),
- exc,
+ 'Не удалось закодировать ссылку подписки в base64 для приложения', app=app.get('id'), exc=exc
)
payload = subscription_url
diff --git a/app/handlers/subscription/countries.py b/app/handlers/subscription/countries.py
index bddd1bbf..3e892cad 100644
--- a/app/handlers/subscription/countries.py
+++ b/app/handlers/subscription/countries.py
@@ -146,13 +146,13 @@ async def get_countries_price_by_uuids_fallback(
return total_price, prices_list
except Exception as e:
- logger.error(f'Ошибка fallback функции: {e}')
+ logger.error('Ошибка fallback функции', error=e)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
async def handle_manage_country(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
- logger.info(f'🔍 Управление страной: {callback.data}')
+ logger.info('🔍 Управление страной', callback_data=callback.data)
country_uuid = callback.data.split('_')[2]
@@ -189,7 +189,7 @@ async def handle_manage_country(callback: types.CallbackQuery, db_user: User, db
current_selected.append(country_uuid)
action = 'added'
- logger.info(f'🔍 Страна {country_uuid} {action}')
+ logger.info('🔍 Страна', country_uuid=country_uuid, action=action)
await state.update_data(countries=current_selected)
@@ -214,7 +214,7 @@ async def handle_manage_country(callback: types.CallbackQuery, db_user: User, db
logger.info('✅ Клавиатура обновлена')
except Exception as e:
- logger.error(f'⚠ Ошибка обновления клавиатуры: {e}')
+ logger.error('⚠ Ошибка обновления клавиатуры', error=e)
await callback.answer()
@@ -251,7 +251,7 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
)
return
- logger.info(f'🔧 Добавлено: {added}, Удалено: {removed}')
+ logger.info('🔧 Добавлено: Удалено', added=added, removed=removed)
months_to_pay = get_remaining_months(subscription.end_date)
@@ -298,11 +298,11 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
if added_names:
logger.info(
- 'Стоимость новых серверов: %.2f₽/мес × %s мес = %.2f₽ (скидка %.2f₽)',
- cost_per_month / 100,
- charged_months,
- total_cost / 100,
- total_discount / 100,
+ 'Стоимость новых серверов: ₽/мес × мес = ₽ (скидка ₽)',
+ cost_per_month=cost_per_month / 100,
+ charged_months=charged_months,
+ total_cost=total_cost / 100,
+ total_discount=total_discount / 100,
)
if total_cost > 0 and db_user.balance_kopeks < total_cost:
@@ -377,7 +377,9 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
await add_user_to_servers(db, added_server_ids)
logger.info(
- f'📊 Добавлены серверы с ценами за {charged_months} мес: {list(zip(added_server_ids, added_server_prices, strict=False))}'
+ '📊 Добавлены серверы с ценами за мес',
+ charged_months=charged_months,
+ value=list(zip(added_server_ids, added_server_prices, strict=False)),
)
subscription.connected_squads = selected_countries
@@ -397,7 +399,7 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
db, db_user, subscription, 'servers', current_countries, selected_countries, total_cost
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления об изменении серверов: {e}')
+ logger.error('Ошибка отправки уведомления об изменении серверов', error=e)
success_text = texts.t(
'COUNTRY_CHANGES_SUCCESS_HEADER',
@@ -454,11 +456,15 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
await state.clear()
logger.info(
- f'✅ Пользователь {db_user.telegram_id} обновил страны. Добавлено: {len(added)}, удалено: {len(removed)}, заплатил: {total_cost / 100}₽'
+ '✅ Пользователь обновил страны. Добавлено: удалено: заплатил: ₽',
+ telegram_id=db_user.telegram_id,
+ added_count=len(added),
+ removed_count=len(removed),
+ total_cost=total_cost / 100,
)
except Exception as e:
- logger.error(f'⚠️ Ошибка применения изменений: {e}')
+ logger.error('⚠️ Ошибка применения изменений', error=e)
await callback.message.edit_text(texts.ERROR, reply_markup=get_back_keyboard(db_user.language))
await callback.answer()
@@ -557,8 +563,7 @@ async def _get_available_countries(promo_group_id: int | None = None):
if promo_group_id is not None and not available_servers:
logger.info(
- 'Промогруппа %s не имеет доступных серверов, возврат пустого списка',
- promo_group_id,
+ 'Промогруппа не имеет доступных серверов, возврат пустого списка', promo_group_id=promo_group_id
)
await cache.set(cache_key_value, [], 60)
return []
@@ -613,7 +618,7 @@ async def _get_available_countries(promo_group_id: int | None = None):
return countries
except Exception as e:
- logger.error(f'Ошибка получения списка стран: {e}')
+ logger.error('Ошибка получения списка стран', error=e)
fallback_countries = [
{
'uuid': 'default-free',
@@ -662,15 +667,15 @@ def _build_countries_selection_text(countries: list[dict], base_text: str) -> st
async def handle_add_country_to_subscription(
callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext
):
- logger.info(f'🔍 handle_add_country_to_subscription вызван для {db_user.telegram_id}')
- logger.info(f'🔍 Callback data: {callback.data}')
+ logger.info('🔍 handle_add_country_to_subscription вызван для', telegram_id=db_user.telegram_id)
+ logger.info('🔍 Callback data', callback_data=callback.data)
current_state = await state.get_state()
- logger.info(f'🔍 Текущее состояние: {current_state}')
+ logger.info('🔍 Текущее состояние', current_state=current_state)
country_uuid = callback.data.split('_')[1]
data = await state.get_data()
- logger.info(f'🔍 Данные состояния: {data}')
+ logger.info('🔍 Данные состояния', data=data)
selected_countries = data.get('countries', [])
countries = await _get_available_countries(db_user.promo_group_id)
@@ -682,10 +687,10 @@ async def handle_add_country_to_subscription(
if country_uuid in selected_countries:
selected_countries.remove(country_uuid)
- logger.info(f'🔍 Удалена страна: {country_uuid}')
+ logger.info('🔍 Удалена страна', country_uuid=country_uuid)
else:
selected_countries.append(country_uuid)
- logger.info(f'🔍 Добавлена страна: {country_uuid}')
+ logger.info('🔍 Добавлена страна', country_uuid=country_uuid)
total_price = 0
subscription = db_user.subscription
@@ -715,8 +720,8 @@ async def handle_add_country_to_subscription(
data['total_price'] = total_price
await state.set_data(data)
- logger.info(f'🔍 Новые выбранные страны: {selected_countries}')
- logger.info(f'🔍 Общая стоимость: {total_price}')
+ logger.info('🔍 Новые выбранные страны', selected_countries=selected_countries)
+ logger.info('🔍 Общая стоимость', total_price=total_price)
try:
from app.keyboards.inline import get_manage_countries_keyboard
@@ -733,7 +738,7 @@ async def handle_add_country_to_subscription(
)
logger.info('✅ Клавиатура обновлена')
except Exception as e:
- logger.error(f'❌ Ошибка обновления клавиатуры: {e}')
+ logger.error('❌ Ошибка обновления клавиатуры', error=e)
await callback.answer()
@@ -751,16 +756,16 @@ async def _should_show_countries_management(user: User | None = None) -> bool:
if allowed_servers:
if len(allowed_servers) > 1:
logger.debug(
- 'Промогруппа %s имеет %s доступных серверов, показываем управление странами',
- promo_group.id,
- len(allowed_servers),
+ 'Промогруппа имеет доступных серверов, показываем управление странами',
+ promo_group_id=promo_group.id,
+ allowed_servers_count=len(allowed_servers),
)
return True
logger.debug(
- 'Промогруппа %s имеет всего %s доступный сервер, пропускаем шаг выбора стран',
- promo_group.id,
- len(allowed_servers),
+ 'Промогруппа имеет всего доступный сервер, пропускаем шаг выбора стран',
+ promo_group_id=promo_group.id,
+ allowed_servers_count=len(allowed_servers),
)
return False
@@ -768,7 +773,7 @@ async def _should_show_countries_management(user: User | None = None) -> bool:
available_countries = [c for c in countries if c.get('is_available', True)]
return len(available_countries) > 1
except Exception as e:
- logger.error(f'Ошибка проверки доступных серверов: {e}')
+ logger.error('Ошибка проверки доступных серверов', error=e)
return True
@@ -925,11 +930,14 @@ async def confirm_add_countries_to_subscription(
await callback.message.edit_text(success_text, reply_markup=get_back_keyboard(db_user.language))
logger.info(
- f'✅ Пользователь {db_user.telegram_id} обновил страны подписки. Добавлено: {len(new_countries)}, убрано: {len(removed_countries)}'
+ '✅ Пользователь обновил страны подписки. Добавлено: убрано',
+ telegram_id=db_user.telegram_id,
+ new_countries_count=len(new_countries),
+ removed_countries_count=len(removed_countries),
)
except Exception as e:
- logger.error(f'Ошибка обновления стран подписки: {e}')
+ logger.error('Ошибка обновления стран подписки', error=e)
await callback.message.edit_text(texts.ERROR, reply_markup=get_back_keyboard(db_user.language))
await state.clear()
diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py
index 396a8b11..df12930a 100644
--- a/app/handlers/subscription/devices.py
+++ b/app/handlers/subscription/devices.py
@@ -66,7 +66,7 @@ async def get_current_devices_detailed(db_user: User) -> dict:
return {'count': 0, 'devices': []}
except Exception as e:
- logger.error(f'Ошибка получения детальной информации об устройствах: {e}')
+ logger.error('Ошибка получения детальной информации об устройствах', error=e)
return {'count': 0, 'devices': []}
@@ -85,9 +85,9 @@ async def get_servers_display_names(squad_uuids: list[str]) -> str:
server = await get_server_squad_by_uuid(db, uuid)
if server:
server_names.append(server.display_name)
- logger.debug(f'Найден сервер в БД: {uuid} -> {server.display_name}')
+ logger.debug('Найден сервер в БД', uuid=uuid, display_name=server.display_name)
else:
- logger.warning(f'Сервер с UUID {uuid} не найден в БД')
+ logger.warning('Сервер с UUID не найден в БД', uuid=uuid)
if not server_names:
countries = await _get_available_countries()
@@ -95,7 +95,7 @@ async def get_servers_display_names(squad_uuids: list[str]) -> str:
for country in countries:
if country['uuid'] == uuid:
server_names.append(country['name'])
- logger.debug(f'Найден сервер в кэше: {uuid} -> {country["name"]}')
+ logger.debug('Найден сервер в кэше', uuid=uuid, country=country['name'])
break
if not server_names:
@@ -110,7 +110,7 @@ async def get_servers_display_names(squad_uuids: list[str]) -> str:
return ', '.join(server_names)
except Exception as e:
- logger.error(f'Ошибка получения названий серверов: {e}')
+ logger.error('Ошибка получения названий серверов', error=e)
if len(squad_uuids) == 1:
return '🎯 Тестовый сервер'
return f'{len(squad_uuids)} стран'
@@ -134,7 +134,7 @@ async def get_current_devices_count(db_user: User) -> str:
return '—'
except Exception as e:
- logger.error(f'Ошибка получения количества устройств: {e}')
+ logger.error('Ошибка получения количества устройств', error=e)
return '—'
@@ -387,10 +387,10 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
},
)
logger.info(
- 'Сохранена корзина add_devices для пользователя %s: +%s устройств, цена %s коп.',
- db_user.telegram_id,
- devices_difference,
- price,
+ 'Сохранена корзина add_devices для пользователя : + устройств, цена коп.',
+ telegram_id=db_user.telegram_id,
+ devices_difference=devices_difference,
+ price=price,
)
await callback.message.answer(
@@ -457,7 +457,7 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
),
).format(connected=connected_count, new=new_devices_count)
except Exception as e:
- logger.error(f'Ошибка проверки устройств: {e}')
+ logger.error('Ошибка проверки устройств', error=e)
confirm_text = texts.t(
'DEVICE_CHANGE_CONFIRMATION',
@@ -577,9 +577,10 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
if connected_count > new_devices_count:
devices_to_remove = connected_count - new_devices_count
logger.info(
- f'🔧 Удаление лишних устройств при уменьшении лимита: '
- f'подключено {connected_count}, новый лимит {new_devices_count}, '
- f'удаляем {devices_to_remove}'
+ '🔧 Удаление лишних устройств при уменьшении лимита: подключено новый лимит удаляем',
+ connected_count=connected_count,
+ new_devices_count=new_devices_count,
+ devices_to_remove=devices_to_remove,
)
# Сортируем по дате (последние в конце) и удаляем последние
@@ -596,11 +597,13 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
delete_data = {'userUuid': db_user.remnawave_uuid, 'hwid': device_hwid}
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
devices_reset_count += 1
- logger.info(f'✅ Удалено устройство {device_hwid}')
+ logger.info('✅ Удалено устройство', device_hwid=device_hwid)
except Exception as del_error:
- logger.error(f'Ошибка удаления устройства {device_hwid}: {del_error}')
+ logger.error(
+ 'Ошибка удаления устройства', device_hwid=device_hwid, del_error=del_error
+ )
except Exception as reset_error:
- logger.error(f'Ошибка удаления устройств при уменьшении лимита: {reset_error}')
+ logger.error('Ошибка удаления устройств при уменьшении лимита', reset_error=reset_error)
await db.refresh(db_user)
await db.refresh(subscription)
@@ -613,7 +616,7 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
db, db_user, subscription, 'devices', current_devices, new_devices_count, price
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления об изменении устройств: {e}')
+ logger.error('Ошибка отправки уведомления об изменении устройств', error=e)
if new_devices_count > current_devices:
success_text = texts.t(
@@ -651,11 +654,15 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
await callback.message.edit_text(success_text, reply_markup=get_back_keyboard(db_user.language))
logger.info(
- f'✅ Пользователь {db_user.telegram_id} изменил количество устройств с {current_devices} на {new_devices_count}, доплата: {price / 100}₽'
+ '✅ Пользователь изменил количество устройств с на доплата: ₽',
+ telegram_id=db_user.telegram_id,
+ current_devices=current_devices,
+ new_devices_count=new_devices_count,
+ price=price / 100,
)
except Exception as e:
- logger.error(f'Ошибка изменения количества устройств: {e}')
+ logger.error('Ошибка изменения количества устройств', error=e)
await callback.message.edit_text(texts.ERROR, reply_markup=get_back_keyboard(db_user.language))
await callback.answer()
@@ -711,7 +718,7 @@ async def handle_device_management(callback: types.CallbackQuery, db_user: User,
)
except Exception as e:
- logger.error(f'Ошибка получения списка устройств: {e}')
+ logger.error('Ошибка получения списка устройств', error=e)
await callback.answer(
texts.t(
'DEVICE_FETCH_INFO_ERROR',
@@ -790,7 +797,7 @@ async def handle_devices_page(callback: types.CallbackQuery, db_user: User, db:
)
except Exception as e:
- logger.error(f'Ошибка перехода на страницу устройств: {e}')
+ logger.error('Ошибка перехода на страницу устройств', error=e)
await callback.answer(
texts.t('DEVICE_PAGE_LOAD_ERROR', '❌ Ошибка загрузки страницы'),
show_alert=True,
@@ -801,7 +808,7 @@ async def handle_single_device_reset(callback: types.CallbackQuery, db_user: Use
try:
callback_parts = callback.data.split('_')
if len(callback_parts) < 4:
- logger.error(f'Некорректный формат callback_data: {callback.data}')
+ logger.error('Некорректный формат callback_data', callback_data=callback.data)
await callback.answer(
texts.t('DEVICE_RESET_INVALID_REQUEST', '❌ Ошибка: некорректный запрос'),
show_alert=True,
@@ -811,10 +818,10 @@ async def handle_single_device_reset(callback: types.CallbackQuery, db_user: Use
device_index = int(callback_parts[2])
page = int(callback_parts[3])
- logger.info(f'🔧 Сброс устройства: index={device_index}, page={page}')
+ logger.info('🔧 Сброс устройства: index=, page', device_index=device_index, page=page)
except (ValueError, IndexError) as e:
- logger.error(f'❌ Ошибка парсинга callback_data {callback.data}: {e}')
+ logger.error('❌ Ошибка парсинга callback_data', callback_data=callback.data, error=e)
await callback.answer(
texts.t('DEVICE_RESET_PARSE_ERROR', '❌ Ошибка обработки запроса'),
show_alert=True,
@@ -879,7 +886,11 @@ async def handle_single_device_reset(callback: types.CallbackQuery, db_user: Use
reply_markup=get_back_keyboard(db_user.language),
)
- logger.info(f'✅ Пользователь {db_user.telegram_id} сбросил устройство {device_info}')
+ logger.info(
+ '✅ Пользователь сбросил устройство',
+ telegram_id=db_user.telegram_id,
+ device_info=device_info,
+ )
else:
await callback.answer(
texts.t(
@@ -900,7 +911,7 @@ async def handle_single_device_reset(callback: types.CallbackQuery, db_user: Use
)
except Exception as e:
- logger.error(f'Ошибка сброса устройства: {e}')
+ logger.error('Ошибка сброса устройства', error=e)
await callback.answer(
texts.t('DEVICE_RESET_ERROR', '❌ Ошибка сброса устройства'),
show_alert=True,
@@ -944,7 +955,7 @@ async def handle_all_devices_reset_from_management(callback: types.CallbackQuery
)
return
- logger.info(f'🔧 Найдено {len(devices_list)} устройств для сброса')
+ logger.info('🔧 Найдено устройств для сброса', devices_list_count=len(devices_list))
success_count = 0
failed_count = 0
@@ -957,14 +968,16 @@ async def handle_all_devices_reset_from_management(callback: types.CallbackQuery
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
success_count += 1
- logger.info(f'✅ Устройство {device_hwid} удалено')
+ logger.info('✅ Устройство удалено', device_hwid=device_hwid)
except Exception as device_error:
failed_count += 1
- logger.error(f'❌ Ошибка удаления устройства {device_hwid}: {device_error}')
+ logger.error(
+ '❌ Ошибка удаления устройства', device_hwid=device_hwid, device_error=device_error
+ )
else:
failed_count += 1
- logger.warning(f'⚠️ У устройства нет HWID: {device}')
+ logger.warning('⚠️ У устройства нет HWID', device=device)
if success_count > 0:
if failed_count == 0:
@@ -981,7 +994,11 @@ async def handle_all_devices_reset_from_management(callback: types.CallbackQuery
reply_markup=get_back_keyboard(db_user.language),
parse_mode='HTML',
)
- logger.info(f'✅ Пользователь {db_user.telegram_id} успешно сбросил {success_count} устройств')
+ logger.info(
+ '✅ Пользователь успешно сбросил устройств',
+ telegram_id=db_user.telegram_id,
+ success_count=success_count,
+ )
else:
await callback.message.edit_text(
texts.t(
@@ -997,7 +1014,10 @@ async def handle_all_devices_reset_from_management(callback: types.CallbackQuery
parse_mode='HTML',
)
logger.warning(
- f'⚠️ Частичный сброс у пользователя {db_user.telegram_id}: {success_count}/{len(devices_list)}'
+ '⚠️ Частичный сброс у пользователя /',
+ telegram_id=db_user.telegram_id,
+ success_count=success_count,
+ devices_list_count=len(devices_list),
)
else:
await callback.message.edit_text(
@@ -1012,10 +1032,12 @@ async def handle_all_devices_reset_from_management(callback: types.CallbackQuery
reply_markup=get_back_keyboard(db_user.language),
parse_mode='HTML',
)
- logger.error(f'❌ Не удалось сбросить ни одного устройства у пользователя {db_user.telegram_id}')
+ logger.error(
+ '❌ Не удалось сбросить ни одного устройства у пользователя', telegram_id=db_user.telegram_id
+ )
except Exception as e:
- logger.error(f'Ошибка сброса всех устройств: {e}')
+ logger.error('Ошибка сброса всех устройств', error=e)
await callback.message.edit_text(texts.ERROR, reply_markup=get_back_keyboard(db_user.language))
await callback.answer()
@@ -1111,12 +1133,12 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
period_label = f'{charged_months} мес'
logger.info(
- 'Добавление %s устройств: %.2f₽/мес × %s = %.2f₽ (скидка %.2f₽)',
- devices_count,
- discounted_per_month / 100,
- period_label,
- price / 100,
- total_discount / 100,
+ 'Добавление устройств: ₽/мес × = ₽ (скидка ₽)',
+ devices_count=devices_count,
+ discounted_per_month=discounted_per_month / 100,
+ period_label=period_label,
+ price=price / 100,
+ total_discount=total_discount / 100,
)
if db_user.balance_kopeks < price:
@@ -1147,10 +1169,10 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
},
)
logger.info(
- 'Сохранена корзина add_devices для пользователя %s: +%s устройств, цена %s коп.',
- db_user.telegram_id,
- devices_count,
- price,
+ 'Сохранена корзина add_devices для пользователя : + устройств, цена коп.',
+ telegram_id=db_user.telegram_id,
+ devices_count=devices_count,
+ price=price,
)
await callback.message.edit_text(
@@ -1201,7 +1223,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
db, db_user, subscription, 'devices', old_device_limit, subscription.device_limit, price
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления о докупке устройств: {e}')
+ logger.error('Ошибка отправки уведомления о докупке устройств', error=e)
success_text = (
'✅ Устройства успешно добавлены!\n\n'
@@ -1214,10 +1236,15 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
await callback.message.edit_text(success_text, reply_markup=get_back_keyboard(db_user.language))
- logger.info(f'✅ Пользователь {db_user.telegram_id} добавил {devices_count} устройств за {price / 100}₽')
+ logger.info(
+ '✅ Пользователь добавил устройств за ₽',
+ telegram_id=db_user.telegram_id,
+ devices_count=devices_count,
+ price=price / 100,
+ )
except Exception as e:
- logger.error(f'Ошибка добавления устройств: {e}')
+ logger.error('Ошибка добавления устройств', error=e)
await callback.message.edit_text(texts.ERROR, reply_markup=get_back_keyboard(db_user.language))
await callback.answer()
diff --git a/app/handlers/subscription/notifications.py b/app/handlers/subscription/notifications.py
index 83d3db11..37e5a99d 100644
--- a/app/handlers/subscription/notifications.py
+++ b/app/handlers/subscription/notifications.py
@@ -16,7 +16,7 @@ async def send_trial_notification(
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_trial_activation_notification(db, db_user, subscription)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления о триале: {e}')
+ logger.error('Ошибка отправки уведомления о триале', error=e)
async def send_purchase_notification(
@@ -38,7 +38,7 @@ async def send_purchase_notification(
db, db_user, subscription, transaction, period_days, was_trial_conversion
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления о покупке: {e}')
+ logger.error('Ошибка отправки уведомления о покупке', error=e)
async def send_extension_notification(
@@ -60,4 +60,4 @@ async def send_extension_notification(
db, db_user, subscription, transaction, extended_days, old_end_date
)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления о продлении: {e}')
+ logger.error('Ошибка отправки уведомления о продлении', error=e)
diff --git a/app/handlers/subscription/pricing.py b/app/handlers/subscription/pricing.py
index 7d935dee..f491160d 100644
--- a/app/handlers/subscription/pricing.py
+++ b/app/handlers/subscription/pricing.py
@@ -361,24 +361,24 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
- logger.info(f'📊 Месячная стоимость конфигурации подписки {subscription.id}:')
+ logger.info('📊 Месячная стоимость конфигурации подписки', subscription_id=subscription.id)
base_log = f' 📅 Базовый тариф (30 дней): {base_cost_original / 100}₽'
if period_discount_percent > 0:
discount_value = base_cost_original * period_discount_percent // 100
base_log += f' → {base_cost / 100}₽ (скидка {period_discount_percent}%: -{discount_value / 100}₽)'
logger.info(base_log)
if servers_cost > 0:
- logger.info(f' 🌍 Серверы: {servers_cost / 100}₽')
+ logger.info('🌍 Серверы: ₽', servers_cost=servers_cost / 100)
if traffic_cost > 0:
- logger.info(f' 📊 Трафик: {traffic_cost / 100}₽')
+ logger.info('📊 Трафик: ₽', traffic_cost=traffic_cost / 100)
if devices_cost > 0:
- logger.info(f' 📱 Устройства: {devices_cost / 100}₽')
- logger.info(f' 💎 ИТОГО: {total_cost / 100}₽')
+ logger.info('📱 Устройства: ₽', devices_cost=devices_cost / 100)
+ logger.info('💎 ИТОГО: ₽', total_cost=total_cost / 100)
return total_cost
except Exception as e:
- logger.error(f'⚠️ Ошибка расчета стоимости подписки: {e}')
+ logger.error('⚠️ Ошибка расчета стоимости подписки', error=e)
return 0
diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py
index 18ca0867..c3dea8d1 100644
--- a/app/handlers/subscription/purchase.py
+++ b/app/handlers/subscription/purchase.py
@@ -1,10 +1,7 @@
-import logging
from datetime import datetime, timedelta
-
-
-logger = logging.getLogger(__name__)
from typing import Any
+import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -58,7 +55,7 @@ from app.services.user_cart_service import user_cart_service
from app.utils.decorators import error_handler
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _serialize_markup(markup: InlineKeyboardMarkup | None) -> Any | None:
@@ -202,7 +199,9 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
# Проверяем и синхронизируем подписку с RemnaWave если необходимо
sync_success, sync_error = await subscription_service.ensure_subscription_synced(db, subscription)
if not sync_success:
- logger.warning(f'Не удалось синхронизировать подписку {subscription.id} с RemnaWave: {sync_error}')
+ logger.warning(
+ 'Не удалось синхронизировать подписку с RemnaWave', subscription_id=subscription.id, sync_error=sync_error
+ )
await db.refresh(subscription)
await db.refresh(db_user)
@@ -294,12 +293,18 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
devices_count = devices_info.get('total', 0)
devices_list = devices_info.get('devices', [])
devices_used_str = str(devices_count)
- logger.info(f'Найдено {devices_count} устройств для пользователя {db_user.telegram_id}')
+ logger.info(
+ 'Найдено устройств для пользователя',
+ devices_count=devices_count,
+ telegram_id=db_user.telegram_id,
+ )
else:
- logger.warning(f'Не удалось получить информацию об устройствах для {db_user.telegram_id}')
+ logger.warning(
+ 'Не удалось получить информацию об устройствах для', telegram_id=db_user.telegram_id
+ )
except Exception as e:
- logger.error(f'Ошибка получения устройств для отображения: {e}')
+ logger.error('Ошибка получения устройств для отображения', error=e)
devices_used = await get_current_devices_count(db_user)
devices_used_str = str(devices_used)
@@ -385,7 +390,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
tariff_info_block = '\n
' + '\n'.join(tariff_info_lines) + '' except Exception as e: - logger.warning(f'Ошибка получения тарифа: {e}', exc_info=True) + logger.warning('Ошибка получения тарифа', error=e, exc_info=True) # Определяем, суточный ли тариф для выбора шаблона is_daily_tariff = tariff and getattr(tariff, 'is_daily', False) @@ -604,9 +609,9 @@ async def show_trial_offer(callback: types.CallbackQuery, db_user: User, db: Asy tariff_trial_days = getattr(trial_tariff, 'trial_duration_days', None) if tariff_trial_days: trial_days = tariff_trial_days - logger.info(f'Показываем триал с тарифом {trial_tariff.name}') + logger.info('Показываем триал с тарифом', trial_tariff_name=trial_tariff.name) except Exception as e: - logger.error(f'Ошибка получения триального тарифа: {e}') + logger.error('Ошибка получения триального тарифа', error=e) try: from app.database.crud.server_squad import get_trial_eligible_server_squads @@ -638,7 +643,7 @@ async def show_trial_offer(callback: types.CallbackQuery, db_user: User, db: Asy logger.warning('Не настроены сквады для выдачи триалов') except Exception as e: - logger.error(f'Ошибка получения триального сервера: {e}') + logger.error('Ошибка получения триального сервера', error=e) if not settings.is_devices_selection_enabled(): forced_limit = settings.get_disabled_mode_device_limit() @@ -859,9 +864,13 @@ async def activate_trial(callback: types.CallbackQuery, db_user: User, db: Async tariff_trial_days = getattr(trial_tariff, 'trial_duration_days', None) if tariff_trial_days: trial_duration = tariff_trial_days - logger.info(f'Используем триальный тариф {trial_tariff.name} (ID: {trial_tariff.id})') + logger.info( + 'Используем триальный тариф (ID: )', + trial_tariff_name=trial_tariff.name, + trial_tariff_id=trial_tariff.id, + ) except Exception as e: - logger.error(f'Ошибка получения триального тарифа: {e}') + logger.error('Ошибка получения триального тарифа', error=e) subscription = await create_trial_subscription( db, @@ -895,9 +904,7 @@ async def activate_trial(callback: types.CallbackQuery, db_user: User, db: Async return logger.error( - 'Insufficient funds detected after trial creation for user %s: %s', - db_user.id, - error, + 'Insufficient funds detected after trial creation for user', db_user_id=db_user.id, error=error ) required_label = settings.format_price(error.required_amount) balance_label = settings.format_price(error.balance_amount) @@ -951,7 +958,7 @@ async def activate_trial(callback: types.CallbackQuery, db_user: User, db: Async subscription, ) except RemnaWaveConfigurationError as error: - logger.error('RemnaWave update skipped due to configuration error: %s', error) + logger.error('RemnaWave update skipped due to configuration error', error=error) revert_result = await revert_trial_activation( db, db_user, @@ -983,9 +990,9 @@ async def activate_trial(callback: types.CallbackQuery, db_user: User, db: Async return except Exception as error: logger.error( - 'Failed to create RemnaWave user for trial subscription %s: %s', - getattr(subscription, 'id', '
{node_name}' if node_name else ''
- logger.info(f'WiFi notification: node_name={node_name!r}, node_info={node_info!r}')
+ logger.info('WiFi notification: node_name=, node_info', node_name=repr(node_name), node_info=repr(node_info))
# Безопасное форматирование
format_vars = {'ban_minutes': ban_minutes, 'network_info': network_info, 'node_info': node_info}
@@ -287,7 +311,7 @@ class BanNotificationService:
reason=reason,
)
if success:
- logger.info(f'Email WiFi уведомление отправлено пользователю {user.id}')
+ logger.info('Email WiFi уведомление отправлено пользователю', user_id=user.id)
return True, 'Email уведомление отправлено', None
return False, 'Не удалось отправить email уведомление', None
@@ -296,12 +320,19 @@ class BanNotificationService:
await self._bot.send_message(
chat_id=user.telegram_id, text=message_text, parse_mode='HTML', reply_markup=get_delete_keyboard()
)
- logger.info(f'Уведомление о WiFi бане отправлено пользователю {username} (telegram_id: {user.telegram_id})')
+ logger.info(
+ 'Уведомление о WiFi бане отправлено пользователю (telegram_id: )',
+ username=username,
+ telegram_id=user.telegram_id,
+ )
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
- f'Ошибка отправки WiFi уведомления пользователю {username} (telegram_id: {user.telegram_id}): {e}'
+ 'Ошибка отправки WiFi уведомления пользователю (telegram_id: )',
+ username=username,
+ telegram_id=user.telegram_id,
+ error=e,
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
@@ -326,7 +357,7 @@ class BanNotificationService:
# Находим пользователя
user = await self._find_user_by_identifier(db, user_identifier)
if not user:
- logger.warning(f'Пользователь {user_identifier} не найден в базе данных')
+ logger.warning('Пользователь не найден в базе данных', user_identifier=user_identifier)
return False, f'Пользователь не найден: {user_identifier}', None
# Формируем сообщение из настроек (заметно выделяем)
@@ -355,7 +386,7 @@ class BanNotificationService:
reason=reason,
)
if success:
- logger.info(f'Email Mobile уведомление отправлено пользователю {user.id}')
+ logger.info('Email Mobile уведомление отправлено пользователю', user_id=user.id)
return True, 'Email уведомление отправлено', None
return False, 'Не удалось отправить email уведомление', None
@@ -365,13 +396,18 @@ class BanNotificationService:
chat_id=user.telegram_id, text=message_text, parse_mode='HTML', reply_markup=get_delete_keyboard()
)
logger.info(
- f'Уведомление о Mobile бане отправлено пользователю {username} (telegram_id: {user.telegram_id})'
+ 'Уведомление о Mobile бане отправлено пользователю (telegram_id: )',
+ username=username,
+ telegram_id=user.telegram_id,
)
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
- f'Ошибка отправки Mobile уведомления пользователю {username} (telegram_id: {user.telegram_id}): {e}'
+ 'Ошибка отправки Mobile уведомления пользователю (telegram_id: )',
+ username=username,
+ telegram_id=user.telegram_id,
+ error=e,
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
diff --git a/app/services/blacklist_service.py b/app/services/blacklist_service.py
index 2b5a8803..a9fc7026 100644
--- a/app/services/blacklist_service.py
+++ b/app/services/blacklist_service.py
@@ -4,16 +4,16 @@
"""
import asyncio
-import logging
import time
from datetime import datetime, timedelta
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class BlacklistService:
@@ -72,7 +72,7 @@ class BlacklistService:
# Получаем содержимое файла
async with aiohttp.ClientSession() as session, session.get(raw_url) as response:
if response.status != 200:
- logger.error(f'Ошибка при получении черного списка: статус {response.status}')
+ logger.error('Ошибка при получении черного списка: статус', status=response.status)
return False
content = await response.text()
@@ -116,20 +116,22 @@ class BlacklistService:
except ValueError:
# Если не удается преобразовать в число, это не ID
logger.warning(
- f'Неверный формат строки {line_num} в черном списке - первое значение не является числом: {line}'
+ 'Неверный формат строки в черном списке первое значение не является числом',
+ line_num=line_num,
+ line=line,
)
self.blacklist_data = blacklist_data
self.last_update = datetime.utcnow()
self._check_cache.clear()
- logger.info(f'Черный список успешно обновлен. Найдено {len(blacklist_data)} записей')
+ logger.info('Черный список успешно обновлен. Найдено записей', blacklist_data_count=len(blacklist_data))
return True
except ValueError as e:
- logger.error(f'Ошибка при парсинге ID из черного списка: {e}')
+ logger.error('Ошибка при парсинге ID из черного списка', error=e)
return False
except Exception as e:
- logger.error(f'Ошибка при обновлении черного списка: {e}')
+ logger.error('Ошибка при обновлении черного списка', error=e)
return False
async def is_user_blacklisted(self, telegram_id: int, username: str | None = None) -> tuple[bool, str | None]:
@@ -168,7 +170,7 @@ class BlacklistService:
# Проверяем по Telegram ID
for bl_id, bl_username, bl_reason in self.blacklist_data:
if bl_id == telegram_id:
- logger.info(f'Пользователь {telegram_id} найден в черном списке по ID: {bl_reason}')
+ logger.info('Пользователь найден в черном списке по ID', telegram_id=telegram_id, bl_reason=bl_reason)
self._check_cache[telegram_id] = (True, bl_reason, now)
return True, bl_reason
@@ -178,7 +180,10 @@ class BlacklistService:
for bl_id, bl_username, bl_reason in self.blacklist_data:
if bl_username and bl_username.lower().lstrip('@') == username_lower:
logger.info(
- f'Пользователь {username} ({telegram_id}) найден в черном списке по username: {bl_reason}'
+ 'Пользователь найден в черном списке по username',
+ username=username,
+ telegram_id=telegram_id,
+ bl_reason=bl_reason,
)
self._check_cache[telegram_id] = (True, bl_reason, now)
return True, bl_reason
diff --git a/app/services/blocked_users_service.py b/app/services/blocked_users_service.py
index 1df8ed64..5b398846 100644
--- a/app/services/blocked_users_service.py
+++ b/app/services/blocked_users_service.py
@@ -6,12 +6,12 @@
"""
import asyncio
-import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
+import structlog
from aiogram import Bot
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramForbiddenError
from sqlalchemy import delete, select
@@ -56,7 +56,7 @@ from app.database.models import (
from app.services.remnawave_service import RemnaWaveService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class BlockCheckStatus(Enum):
@@ -148,13 +148,13 @@ class BlockedUsersService:
if 'chat not found' in error_lower or 'user not found' in error_lower:
# Пользователь удалил аккаунт или никогда не начинал диалог
return BlockCheckStatus.BLOCKED
- logger.warning(f'TelegramBadRequest при проверке {telegram_id}: {e}')
+ logger.warning('TelegramBadRequest при проверке', telegram_id=telegram_id, error=e)
return BlockCheckStatus.ERROR
except TelegramAPIError as e:
- logger.warning(f'TelegramAPIError при проверке {telegram_id}: {e}')
+ logger.warning('TelegramAPIError при проверке', telegram_id=telegram_id, error=e)
return BlockCheckStatus.ERROR
except Exception as e:
- logger.error(f'Неожиданная ошибка при проверке {telegram_id}: {e}')
+ logger.error('Неожиданная ошибка при проверке', telegram_id=telegram_id, error=e)
return BlockCheckStatus.ERROR
async def _check_single_user(self, user: User) -> BlockCheckResult:
@@ -214,7 +214,7 @@ class BlockedUsersService:
all_users = users_result.scalars().all()
total_users = len(all_users)
- logger.info(f'Начинаем проверку {total_users} пользователей на блокировку бота')
+ logger.info('Начинаем проверку пользователей на блокировку бота', total_users=total_users)
# Проверяем пользователей батчами с ограничением параллелизма
semaphore = asyncio.Semaphore(self.MAX_CONCURRENT_CHECKS)
@@ -234,7 +234,7 @@ class BlockedUsersService:
for check_result in batch_results:
if isinstance(check_result, Exception):
result.errors += 1
- logger.error(f'Ошибка при проверке пользователя: {check_result}')
+ logger.error('Ошибка при проверке пользователя', check_result=check_result)
continue
result.total_checked += 1
@@ -255,8 +255,10 @@ class BlockedUsersService:
result.scan_duration_seconds = (datetime.now(tz=UTC) - start_time).total_seconds()
logger.info(
- f'Сканирование завершено: {result.blocked_count} заблокированных '
- f'из {result.total_checked} проверенных за {result.scan_duration_seconds:.1f}с'
+ 'Сканирование завершено: заблокированных из проверенных за с',
+ blocked_count=result.blocked_count,
+ total_checked=result.total_checked,
+ scan_duration_seconds=round(result.scan_duration_seconds, 1),
)
return result
@@ -273,14 +275,14 @@ class BlockedUsersService:
async with self.remnawave_service.get_api_client() as api:
await api.delete_user(remnawave_uuid)
- logger.info(f'Удален пользователь {remnawave_uuid} из Remnawave')
+ logger.info('Удален пользователь из Remnawave', remnawave_uuid=remnawave_uuid)
return True
except Exception as e:
error_msg = str(e).lower()
if 'not found' in error_msg or '404' in error_msg:
- logger.info(f'Пользователь {remnawave_uuid} уже удален из Remnawave')
+ logger.info('Пользователь уже удален из Remnawave', remnawave_uuid=remnawave_uuid)
return True
- logger.error(f'Ошибка удаления {remnawave_uuid} из Remnawave: {e}')
+ logger.error('Ошибка удаления из Remnawave', remnawave_uuid=remnawave_uuid, error=e)
return False
async def delete_user_from_db(self, db: AsyncSession, user_id: int) -> bool:
@@ -295,7 +297,7 @@ class BlockedUsersService:
user = user_result.scalar_one_or_none()
if not user:
- logger.warning(f'Пользователь {user_id} не найден в БД')
+ logger.warning('Пользователь не найден в БД', user_id=user_id)
return False
user_display = user.telegram_id or user.email or f'#{user.id}'
@@ -360,11 +362,11 @@ class BlockedUsersService:
await db.delete(user)
await db.commit()
- logger.info(f'Пользователь {user_display} полностью удален из БД')
+ logger.info('Пользователь полностью удален из БД', user_display=user_display)
return True
except Exception as e:
- logger.error(f'Ошибка удаления пользователя {user_id} из БД: {e}')
+ logger.error('Ошибка удаления пользователя из БД', user_id=user_id, error=e)
await db.rollback()
return False
@@ -381,11 +383,11 @@ class BlockedUsersService:
user.updated_at = datetime.now(tz=UTC)
await db.commit()
- logger.info(f'Пользователь {user.telegram_id or user.id} помечен как заблокированный')
+ logger.info('Пользователь помечен как заблокированный', telegram_id=user.telegram_id or user.id)
return True
except Exception as e:
- logger.error(f'Ошибка пометки пользователя {user_id}: {e}')
+ logger.error('Ошибка пометки пользователя', user_id=user_id, error=e)
await db.rollback()
return False
diff --git a/app/services/broadcast_service.py b/app/services/broadcast_service.py
index 91310361..a2e3aee5 100644
--- a/app/services/broadcast_service.py
+++ b/app/services/broadcast_service.py
@@ -1,11 +1,11 @@
from __future__ import annotations
import asyncio
-import logging
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING
+import structlog
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
from aiogram.types import InlineKeyboardMarkup
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
from app.cabinet.services.email_service import EmailService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
VALID_MEDIA_TYPES = {'photo', 'video', 'document'}
@@ -103,7 +103,7 @@ class BroadcastService:
async def start_broadcast(self, broadcast_id: int, config: BroadcastConfig) -> None:
if self._bot is None:
- logger.error('Невозможно запустить рассылку %s: бот не инициализирован', broadcast_id)
+ logger.error('Невозможно запустить рассылку : бот не инициализирован', broadcast_id=broadcast_id)
await self._mark_failed(broadcast_id)
return
@@ -111,7 +111,7 @@ class BroadcastService:
async with self._lock:
if broadcast_id in self._tasks and not self._tasks[broadcast_id].task.done():
- logger.warning('Рассылка %s уже запущена', broadcast_id)
+ logger.warning('Рассылка уже запущена', broadcast_id=broadcast_id)
return
task = asyncio.create_task(
@@ -147,7 +147,7 @@ class BroadcastService:
async with AsyncSessionLocal() as session:
broadcast = await session.get(BroadcastHistory, broadcast_id)
if not broadcast:
- logger.error('Запись рассылки %s не найдена в БД', broadcast_id)
+ logger.error('Запись рассылки не найдена в БД', broadcast_id=broadcast_id)
return
broadcast.status = 'in_progress'
@@ -161,7 +161,7 @@ class BroadcastService:
async with AsyncSessionLocal() as session:
broadcast = await session.get(BroadcastHistory, broadcast_id)
if not broadcast:
- logger.error('Запись рассылки %s удалена до запуска', broadcast_id)
+ logger.error('Запись рассылки удалена до запуска', broadcast_id=broadcast_id)
return
broadcast.total_count = len(recipient_ids)
@@ -172,18 +172,18 @@ class BroadcastService:
return
if not recipient_ids:
- logger.info('Рассылка %s: получатели не найдены', broadcast_id)
+ logger.info('Рассылка : получатели не найдены', broadcast_id=broadcast_id)
await self._mark_finished(broadcast_id, sent_count, failed_count, cancelled=False)
return
keyboard = self._build_keyboard(config.selected_buttons)
logger.info(
- 'Рассылка %s: начинаем отправку %d получателям (batch=%d, delay=%.1fs)',
- broadcast_id,
- len(recipient_ids),
- _TG_BATCH_SIZE,
- _TG_BATCH_DELAY,
+ 'Рассылка : начинаем отправку получателям (batch delay=s)',
+ broadcast_id=broadcast_id,
+ recipient_ids_count=len(recipient_ids),
+ TG_BATCH_SIZE=_TG_BATCH_SIZE,
+ TG_BATCH_DELAY=_TG_BATCH_DELAY,
)
sent_count, failed_count, cancelled_during_run = await self._send_batched(
@@ -196,15 +196,15 @@ class BroadcastService:
if cancelled_during_run:
logger.info(
- 'Рассылка %s была отменена во время выполнения, финальный статус уже установлен',
- broadcast_id,
+ 'Рассылка была отменена во время выполнения, финальный статус уже установлен',
+ broadcast_id=broadcast_id,
)
return
if cancel_event.is_set():
logger.info(
- 'Запрос на отмену рассылки %s пришел после завершения отправки, фиксируем итоговый статус',
- broadcast_id,
+ 'Запрос на отмену рассылки пришел после завершения отправки, фиксируем итоговый статус',
+ broadcast_id=broadcast_id,
)
await self._mark_finished(
@@ -218,7 +218,7 @@ class BroadcastService:
await self._mark_cancelled(broadcast_id, sent_count, failed_count)
raise
except Exception as exc:
- logger.exception('Критическая ошибка при выполнении рассылки %s: %s', broadcast_id, exc)
+ logger.exception('Критическая ошибка при выполнении рассылки', broadcast_id=broadcast_id, exc=exc)
await self._mark_failed(broadcast_id, sent_count, failed_count)
async def _fetch_recipients(self, target: str) -> list[int]:
@@ -277,12 +277,12 @@ class BroadcastService:
wait_seconds = e.retry_after + 1
flood_wait_until = asyncio.get_event_loop().time() + wait_seconds
logger.warning(
- 'FloodWait рассылки %s: Telegram просит %d сек (user=%d, попытка %d/%d)',
- broadcast_id,
- e.retry_after,
- telegram_id,
- attempt + 1,
- _TG_MAX_RETRIES,
+ 'FloodWait рассылки : Telegram просит сек (user попытка /)',
+ broadcast_id=broadcast_id,
+ retry_after=e.retry_after,
+ telegram_id=telegram_id,
+ attempt=attempt + 1,
+ TG_MAX_RETRIES=_TG_MAX_RETRIES,
)
await asyncio.sleep(wait_seconds)
@@ -294,12 +294,12 @@ class BroadcastService:
except Exception as exc:
logger.error(
- 'Ошибка отправки рассылки %s пользователю %d (попытка %d/%d): %s',
- broadcast_id,
- telegram_id,
- attempt + 1,
- _TG_MAX_RETRIES,
- exc,
+ 'Ошибка отправки рассылки пользователю (попытка /)',
+ broadcast_id=broadcast_id,
+ telegram_id=telegram_id,
+ attempt=attempt + 1,
+ TG_MAX_RETRIES=_TG_MAX_RETRIES,
+ exc=exc,
)
if attempt < _TG_MAX_RETRIES - 1:
await asyncio.sleep(0.5 * (attempt + 1))
@@ -325,7 +325,7 @@ class BroadcastService:
failed_count += 1
elif isinstance(result, Exception):
failed_count += 1
- logger.error('Необработанное исключение в рассылке %s: %s', broadcast_id, result)
+ logger.error('Необработанное исключение в рассылке', broadcast_id=broadcast_id, result=result)
# Обновляем прогресс в БД периодически
processed = sent_count + failed_count
@@ -474,14 +474,14 @@ class BroadcastService:
except InterfaceError as exc:
attempts += 1
logger.warning(
- 'Проблемы с соединением при обновлении статуса рассылки %s: %s. Повтор %s/2',
- broadcast_id,
- exc,
- attempts,
+ 'Проблемы с соединением при обновлении статуса рассылки : . Повтор /2',
+ broadcast_id=broadcast_id,
+ exc=exc,
+ attempts=attempts,
)
await asyncio.sleep(0.2)
except SQLAlchemyError:
- logger.exception('Не удалось обновить статус рассылки %s', broadcast_id)
+ logger.exception('Не удалось обновить статус рассылки', broadcast_id=broadcast_id)
return
@@ -508,12 +508,12 @@ class EmailBroadcastService:
async def start_broadcast(self, broadcast_id: int, config: EmailBroadcastConfig) -> None:
"""Start email broadcast in background."""
if self._email_service is None:
- logger.error('Cannot start email broadcast %s: email service not initialized', broadcast_id)
+ logger.error('Cannot start email broadcast : email service not initialized', broadcast_id=broadcast_id)
await self._mark_failed(broadcast_id)
return
if not self._email_service.is_configured():
- logger.error('Cannot start email broadcast %s: SMTP not configured', broadcast_id)
+ logger.error('Cannot start email broadcast : SMTP not configured', broadcast_id=broadcast_id)
await self._mark_failed(broadcast_id)
return
@@ -521,7 +521,7 @@ class EmailBroadcastService:
async with self._lock:
if broadcast_id in self._tasks and not self._tasks[broadcast_id].task.done():
- logger.warning('Email broadcast %s is already running', broadcast_id)
+ logger.warning('Email broadcast is already running', broadcast_id=broadcast_id)
return
task = asyncio.create_task(
@@ -560,7 +560,7 @@ class EmailBroadcastService:
async with AsyncSessionLocal() as session:
broadcast = await session.get(BroadcastHistory, broadcast_id)
if not broadcast:
- logger.error('Broadcast record %s not found', broadcast_id)
+ logger.error('Broadcast record not found', broadcast_id=broadcast_id)
return
broadcast.status = 'in_progress'
@@ -575,7 +575,7 @@ class EmailBroadcastService:
async with AsyncSessionLocal() as session:
broadcast = await session.get(BroadcastHistory, broadcast_id)
if not broadcast:
- logger.error('Broadcast record %s deleted before start', broadcast_id)
+ logger.error('Broadcast record deleted before start', broadcast_id=broadcast_id)
return
broadcast.total_count = len(recipients)
@@ -586,7 +586,7 @@ class EmailBroadcastService:
return
if not recipients:
- logger.info('Email broadcast %s: no recipients found', broadcast_id)
+ logger.info('Email broadcast : no recipients found', broadcast_id=broadcast_id)
await self._mark_finished(broadcast_id, sent_count, failed_count, cancelled=False)
return
@@ -599,7 +599,7 @@ class EmailBroadcastService:
)
if was_cancelled:
- logger.info('Email broadcast %s was cancelled during execution', broadcast_id)
+ logger.info('Email broadcast was cancelled during execution', broadcast_id=broadcast_id)
return
await self._mark_finished(broadcast_id, sent_count, failed_count, cancelled=False)
@@ -608,7 +608,7 @@ class EmailBroadcastService:
await self._mark_cancelled(broadcast_id, sent_count, failed_count)
raise
except Exception as exc:
- logger.exception('Critical error in email broadcast %s: %s', broadcast_id, exc)
+ logger.exception('Critical error in email broadcast', broadcast_id=broadcast_id, exc=exc)
await self._mark_failed(broadcast_id, sent_count, failed_count)
async def _fetch_email_recipients(self, target: str) -> list[_EmailRecipient]:
@@ -672,7 +672,7 @@ class EmailBroadcastService:
)
else:
- logger.warning('Unknown email target filter: %s', target)
+ logger.warning('Unknown email target filter', target=target)
return []
# Загружаем батчами и извлекаем скаляры сразу
@@ -748,10 +748,7 @@ class EmailBroadcastService:
return success
except Exception as exc:
logger.error(
- 'Ошибка отправки email рассылки %s на %s: %s',
- broadcast_id,
- recipient.email,
- exc,
+ 'Ошибка отправки email рассылки на', broadcast_id=broadcast_id, email=recipient.email, exc=exc
)
return False
@@ -875,14 +872,14 @@ class EmailBroadcastService:
except InterfaceError as exc:
attempts += 1
logger.warning(
- 'Connection issue updating email broadcast %s: %s. Retry %s/2',
- broadcast_id,
- exc,
- attempts,
+ 'Connection issue updating email broadcast : . Retry /2',
+ broadcast_id=broadcast_id,
+ exc=exc,
+ attempts=attempts,
)
await asyncio.sleep(0.2)
except SQLAlchemyError:
- logger.exception('Failed to update email broadcast status %s', broadcast_id)
+ logger.exception('Failed to update email broadcast status', broadcast_id=broadcast_id)
return
diff --git a/app/services/bulk_ban_service.py b/app/services/bulk_ban_service.py
index 7884201a..4956ff71 100644
--- a/app/services/bulk_ban_service.py
+++ b/app/services/bulk_ban_service.py
@@ -2,8 +2,7 @@
Модуль для массовой блокировки пользователей по списку Telegram ID
"""
-import logging
-
+import structlog
from aiogram import Bot
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,7 +12,7 @@ from app.services.admin_notification_service import AdminNotificationService
from app.services.user_service import UserService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class BulkBanService:
@@ -59,13 +58,13 @@ class BulkBanService:
user = await get_user_by_telegram_id(db, telegram_id)
if not user:
- logger.warning(f'Пользователь с Telegram ID {telegram_id} не найден')
+ logger.warning('Пользователь с Telegram ID не найден', telegram_id=telegram_id)
not_found_users.append(telegram_id)
continue
# Проверяем, что пользователь не заблокирован уже
if user.status == UserStatus.BLOCKED.value:
- logger.info(f'Пользователь {telegram_id} уже заблокирован')
+ logger.info('Пользователь уже заблокирован', telegram_id=telegram_id)
continue
# Блокируем пользователя
@@ -73,7 +72,7 @@ class BulkBanService:
if ban_success:
successfully_banned += 1
- logger.info(f'Пользователь {telegram_id} успешно заблокирован')
+ logger.info('Пользователь успешно заблокирован', telegram_id=telegram_id)
# Отправляем уведомление пользователю, если возможно
if bot:
@@ -89,13 +88,15 @@ class BulkBanService:
parse_mode='HTML',
)
except Exception as e:
- logger.warning(f'Не удалось отправить уведомление пользователю {telegram_id}: {e}')
+ logger.warning(
+ 'Не удалось отправить уведомление пользователю', telegram_id=telegram_id, error=e
+ )
else:
- logger.error(f'Не удалось заблокировать пользователя {telegram_id}')
+ logger.error('Не удалось заблокировать пользователя', telegram_id=telegram_id)
error_ids.append(telegram_id)
except Exception as e:
- logger.error(f'Ошибка при блокировке пользователя {telegram_id}: {e}')
+ logger.error('Ошибка при блокировке пользователя', telegram_id=telegram_id, error=e)
error_ids.append(telegram_id)
# Отправляем уведомление администратору
@@ -106,11 +107,13 @@ class BulkBanService:
admin_user_id, successfully_banned, len(not_found_users), len(error_ids), admin_name
)
except Exception as e:
- logger.error(f'Ошибка при отправке уведомления администратору: {e}')
+ logger.error('Ошибка при отправке уведомления администратору', error=e)
logger.info(
- f'Массовая блокировка завершена: успешно={successfully_banned}, '
- f'не найдено={len(not_found_users)}, ошибки={len(error_ids)}'
+ 'Массовая блокировка завершена: успешно=, не найдено=, ошибки',
+ successfully_banned=successfully_banned,
+ not_found_users_count=len(not_found_users),
+ error_ids_count=len(error_ids),
)
return successfully_banned, len(not_found_users), error_ids
diff --git a/app/services/campaign_service.py b/app/services/campaign_service.py
index ffb9658c..68db2c48 100644
--- a/app/services/campaign_service.py
+++ b/app/services/campaign_service.py
@@ -1,6 +1,6 @@
-import logging
from dataclasses import dataclass
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -15,7 +15,7 @@ from app.database.models import AdvertisingCampaign, User
from app.services.subscription_service import SubscriptionService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _format_user_log(user: User) -> str:
@@ -53,7 +53,7 @@ class AdvertisingCampaignService:
campaign: AdvertisingCampaign,
) -> CampaignBonusResult:
if not campaign.is_active:
- logger.warning('⚠️ Попытка выдать бонус по неактивной кампании %s', campaign.id)
+ logger.warning('⚠️ Попытка выдать бонус по неактивной кампании', campaign_id=campaign.id)
return CampaignBonusResult(success=False)
if campaign.is_balance_bonus:
@@ -68,7 +68,7 @@ class AdvertisingCampaignService:
if campaign.is_tariff_bonus:
return await self._apply_tariff_bonus(db, user, campaign)
- logger.error('❌ Неизвестный тип бонуса кампании: %s', campaign.bonus_type)
+ logger.error('❌ Неизвестный тип бонуса кампании', bonus_type=campaign.bonus_type)
return CampaignBonusResult(success=False)
async def _apply_balance_bonus(
@@ -79,7 +79,7 @@ class AdvertisingCampaignService:
) -> CampaignBonusResult:
amount = campaign.balance_bonus_kopeks or 0
if amount <= 0:
- logger.info('ℹ️ Кампания %s не имеет бонуса на баланс', campaign.id)
+ logger.info('ℹ️ Кампания не имеет бонуса на баланс', campaign_id=campaign.id)
return CampaignBonusResult(success=False)
description = f"Бонус за регистрацию по кампании '{campaign.name}'"
@@ -102,10 +102,10 @@ class AdvertisingCampaignService:
)
logger.info(
- '💰 Пользователю %s начислен бонус %s₽ по кампании %s',
- _format_user_log(user),
- amount / 100,
- campaign.id,
+ '💰 Пользователю начислен бонус ₽ по кампании',
+ format_user_log=_format_user_log(user),
+ amount=amount / 100,
+ campaign_id=campaign.id,
)
return CampaignBonusResult(
@@ -123,18 +123,15 @@ class AdvertisingCampaignService:
existing_subscription = await get_subscription_by_user_id(db, user.id)
if existing_subscription:
logger.warning(
- '⚠️ У пользователя %s уже есть подписка, бонус кампании %s пропущен',
- _format_user_log(user),
- campaign.id,
+ '⚠️ У пользователя уже есть подписка, бонус кампании пропущен',
+ format_user_log=_format_user_log(user),
+ campaign_id=campaign.id,
)
return CampaignBonusResult(success=False)
duration_days = campaign.subscription_duration_days or 0
if duration_days <= 0:
- logger.info(
- 'ℹ️ Кампания %s не содержит корректной длительности подписки',
- campaign.id,
- )
+ logger.info('ℹ️ Кампания не содержит корректной длительности подписки', campaign_id=campaign.id)
return CampaignBonusResult(success=False)
traffic_limit = campaign.subscription_traffic_gb
@@ -151,11 +148,7 @@ class AdvertisingCampaignService:
if trial_uuid:
squads = [trial_uuid]
except Exception as error:
- logger.error(
- 'Не удалось подобрать сквад для кампании %s: %s',
- campaign.id,
- error,
- )
+ logger.error('Не удалось подобрать сквад для кампании', campaign_id=campaign.id, error=error)
squads[0] if squads else None
@@ -173,11 +166,7 @@ class AdvertisingCampaignService:
try:
await self.subscription_service.create_remnawave_user(db, new_subscription)
except Exception as error:
- logger.error(
- '❌ Ошибка синхронизации RemnaWave для кампании %s: %s',
- campaign.id,
- error,
- )
+ logger.error('❌ Ошибка синхронизации RemnaWave для кампании', campaign_id=campaign.id, error=error)
await record_campaign_registration(
db,
@@ -188,10 +177,10 @@ class AdvertisingCampaignService:
)
logger.info(
- '🎁 Пользователю %s выдана подписка по кампании %s на %s дней',
- _format_user_log(user),
- campaign.id,
- duration_days,
+ '🎁 Пользователю выдана подписка по кампании на дней',
+ format_user_log=_format_user_log(user),
+ campaign_id=campaign.id,
+ duration_days=duration_days,
)
return CampaignBonusResult(
@@ -218,9 +207,9 @@ class AdvertisingCampaignService:
)
logger.info(
- '📊 Пользователь %s зарегистрирован по ссылке кампании %s (без награды)',
- _format_user_log(user),
- campaign.id,
+ '📊 Пользователь зарегистрирован по ссылке кампании (без награды)',
+ format_user_log=_format_user_log(user),
+ campaign_id=campaign.id,
)
return CampaignBonusResult(
@@ -238,43 +227,29 @@ class AdvertisingCampaignService:
existing_subscription = await get_subscription_by_user_id(db, user.id)
if existing_subscription:
logger.warning(
- '⚠️ У пользователя %s уже есть подписка, бонус тарифа кампании %s пропущен',
- _format_user_log(user),
- campaign.id,
+ '⚠️ У пользователя уже есть подписка, бонус тарифа кампании пропущен',
+ format_user_log=_format_user_log(user),
+ campaign_id=campaign.id,
)
return CampaignBonusResult(success=False)
if not campaign.tariff_id:
- logger.error(
- '❌ Кампания %s не имеет указанного тарифа для выдачи',
- campaign.id,
- )
+ logger.error('❌ Кампания не имеет указанного тарифа для выдачи', campaign_id=campaign.id)
return CampaignBonusResult(success=False)
duration_days = campaign.tariff_duration_days or 0
if duration_days <= 0:
- logger.error(
- '❌ Кампания %s не имеет указанной длительности тарифа',
- campaign.id,
- )
+ logger.error('❌ Кампания не имеет указанной длительности тарифа', campaign_id=campaign.id)
return CampaignBonusResult(success=False)
# Получаем тариф для извлечения параметров
tariff = await get_tariff_by_id(db, campaign.tariff_id)
if not tariff:
- logger.error(
- '❌ Тариф %s не найден для кампании %s',
- campaign.tariff_id,
- campaign.id,
- )
+ logger.error('❌ Тариф не найден для кампании', tariff_id=campaign.tariff_id, campaign_id=campaign.id)
return CampaignBonusResult(success=False)
if not tariff.is_active:
- logger.warning(
- '⚠️ Тариф %s неактивен, бонус кампании %s пропущен',
- tariff.id,
- campaign.id,
- )
+ logger.warning('⚠️ Тариф неактивен, бонус кампании пропущен', tariff_id=tariff.id, campaign_id=campaign.id)
return CampaignBonusResult(success=False)
traffic_limit = tariff.traffic_limit_gb
@@ -289,11 +264,7 @@ class AdvertisingCampaignService:
if trial_uuid:
squads = [trial_uuid]
except Exception as error:
- logger.error(
- 'Не удалось подобрать сквад для тарифа кампании %s: %s',
- campaign.id,
- error,
- )
+ logger.error('Не удалось подобрать сквад для тарифа кампании', campaign_id=campaign.id, error=error)
# Создаём подписку как платную (не trial) с привязкой к тарифу
new_subscription = await create_paid_subscription(
@@ -311,11 +282,7 @@ class AdvertisingCampaignService:
try:
await self.subscription_service.create_remnawave_user(db, new_subscription)
except Exception as error:
- logger.error(
- '❌ Ошибка синхронизации RemnaWave для тарифа кампании %s: %s',
- campaign.id,
- error,
- )
+ logger.error('❌ Ошибка синхронизации RemnaWave для тарифа кампании', campaign_id=campaign.id, error=error)
await record_campaign_registration(
db,
@@ -327,11 +294,11 @@ class AdvertisingCampaignService:
)
logger.info(
- "🎁 Пользователю %s выдан тариф '%s' по кампании %s на %s дней",
- _format_user_log(user),
- tariff.name,
- campaign.id,
- duration_days,
+ "🎁 Пользователю выдан тариф '' по кампании на дней",
+ format_user_log=_format_user_log(user),
+ tariff_name=tariff.name,
+ campaign_id=campaign.id,
+ duration_days=duration_days,
)
return CampaignBonusResult(
diff --git a/app/services/cloudpayments_service.py b/app/services/cloudpayments_service.py
index ac157036..7173cb01 100644
--- a/app/services/cloudpayments_service.py
+++ b/app/services/cloudpayments_service.py
@@ -5,17 +5,17 @@ from __future__ import annotations
import base64
import hashlib
import hmac
-import logging
import time
from typing import Any
from urllib.parse import unquote_plus
import httpx
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class CloudPaymentsAPIError(RuntimeError):
@@ -83,13 +83,13 @@ class CloudPaymentsService:
data = response.json()
if response.status_code >= 400:
- logger.error('CloudPayments API error %s: %s', response.status_code, data)
+ logger.error('CloudPayments API error', status_code=response.status_code, data=data)
raise CloudPaymentsAPIError(f'CloudPayments API returned status {response.status_code}')
return data
except httpx.RequestError as error:
- logger.error('Error communicating with CloudPayments API: %s', error)
+ logger.error('Error communicating with CloudPayments API', error=error)
raise CloudPaymentsAPIError('Failed to communicate with CloudPayments API') from error
@staticmethod
@@ -167,21 +167,17 @@ class CloudPaymentsService:
if not response.get('Success'):
error_message = response.get('Message', 'Unknown error')
- logger.error('CloudPayments orders/create failed: %s', error_message)
+ logger.error('CloudPayments orders/create failed', error_message=error_message)
raise CloudPaymentsAPIError(f'Failed to create order: {error_message}')
model = response.get('Model', {})
payment_url = model.get('Url')
if not payment_url:
- logger.error('CloudPayments orders/create returned no URL: %s', response)
+ logger.error('CloudPayments orders/create returned no URL', response=response)
raise CloudPaymentsAPIError('CloudPayments API returned no payment URL')
- logger.info(
- 'CloudPayments order created: id=%s, url=%s',
- model.get('Id'),
- payment_url,
- )
+ logger.info('CloudPayments order created: id url', model=model.get('Id'), payment_url=payment_url)
return payment_url
@@ -316,10 +312,10 @@ class CloudPaymentsService:
pass
logger.warning(
- 'CloudPayments signature mismatch: expected_raw=%s..., expected_decoded=%s..., got=%s...',
- calculated_raw[:20],
- calculated_decoded[:20] if calculated_decoded else 'N/A',
- signature[:20],
+ 'CloudPayments signature mismatch: expected_raw=..., expected_decoded=..., got=...',
+ calculated_raw=calculated_raw[:20],
+ calculated_decoded=calculated_decoded[:20] if calculated_decoded else 'N/A',
+ signature=signature[:20],
)
return False
diff --git a/app/services/contest_rotation_service.py b/app/services/contest_rotation_service.py
index 9bd5ab2f..105f9f8f 100644
--- a/app/services/contest_rotation_service.py
+++ b/app/services/contest_rotation_service.py
@@ -1,8 +1,8 @@
import asyncio
-import logging
from datetime import UTC, datetime, time, timedelta
from zoneinfo import ZoneInfo
+import structlog
from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from app.services.contests.enums import GameType, PrizeType
from app.services.contests.games import get_game_strategy
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Legacy aliases for backward compatibility
GAME_QUEST = GameType.QUEST_BUTTONS.value
@@ -166,7 +166,7 @@ class ContestRotationService:
try:
await upsert_template(db, **tpl)
except Exception as exc:
- logger.error('Не удалось создать шаблон %s: %s', tpl['slug'], exc)
+ logger.error('Не удалось создать шаблон', tpl=tpl['slug'], exc=exc)
async def _loop(self) -> None:
try:
@@ -176,7 +176,7 @@ class ContestRotationService:
except asyncio.CancelledError:
raise
except Exception as exc:
- logger.error('Ошибка в ротации конкурсов: %s', exc)
+ logger.error('Ошибка в ротации конкурсов', exc=exc)
await asyncio.sleep(self._interval_seconds)
except asyncio.CancelledError:
logger.info('Сервис ротации конкурсов остановлен')
@@ -234,14 +234,14 @@ class ContestRotationService:
ends_at=ends_at_utc,
payload=payload,
)
- logger.info('Создан раунд %s для шаблона %s', round_obj.id, tpl.slug)
+ logger.info('Создан раунд для шаблона', round_obj_id=round_obj.id, slug=tpl.slug)
def _get_timezone(self) -> ZoneInfo:
tz_name = settings.TIMEZONE or 'UTC'
try:
return ZoneInfo(tz_name)
except Exception:
- logger.warning('Не удалось загрузить TZ %s, используем UTC', tz_name)
+ logger.warning('Не удалось загрузить TZ , используем UTC', tz_name=tz_name)
return ZoneInfo('UTC')
def _build_payload_for_template(self, tpl: ContestTemplate) -> dict:
@@ -315,7 +315,7 @@ class ContestRotationService:
reply_markup=keyboard,
)
except Exception as exc:
- logger.error('Не удалось отправить анонс в канал %s: %s', channel_id_raw, exc)
+ logger.error('Не удалось отправить анонс в канал', channel_id_raw=channel_id_raw, exc=exc)
async def _broadcast_to_users(self, text: str) -> None:
"""Отправляет анонс всем пользователям с активной/триальной подпиской."""
@@ -364,9 +364,9 @@ class ContestRotationService:
await asyncio.gather(*tasks, return_exceptions=True)
- logger.info('Анонс игр: отправлено=%s, ошибок=%s', sent, failed)
+ logger.info('Анонс игр: отправлено ошибок', sent=sent, failed=failed)
except Exception as exc:
- logger.error('Ошибка рассылки анонса игр пользователям: %s', exc)
+ logger.error('Ошибка рассылки анонса игр пользователям', exc=exc)
async def _load_users_batch(self, db: AsyncSession, offset: int, limit: int) -> list[User]:
from app.database.crud.user import get_users_list
diff --git a/app/services/contests/attempt_service.py b/app/services/contests/attempt_service.py
index 0e4db4bc..dcc06485 100644
--- a/app/services/contests/attempt_service.py
+++ b/app/services/contests/attempt_service.py
@@ -1,8 +1,8 @@
"""Service for atomic contest attempt operations."""
-import logging
from dataclasses import dataclass
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -15,7 +15,7 @@ from app.services.contests.enums import PrizeType
from app.services.contests.games import get_game_strategy
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@dataclass
@@ -95,7 +95,13 @@ class ContestAttemptService:
is_winner=is_winner,
)
- logger.info("Contest attempt: user %s, round %s, pick '%s', winner %s", user_id, round_obj.id, pick, is_winner)
+ logger.info(
+ "Contest attempt: user , round , pick '', winner",
+ user_id=user_id,
+ round_obj_id=round_obj.id,
+ pick=pick,
+ is_winner=is_winner,
+ )
if is_winner:
prize_msg = await self._award_prize(db, user_id, tpl, language)
@@ -177,11 +183,11 @@ class ContestAttemptService:
await update_attempt(db, attempt, answer=text_answer.strip().upper(), is_winner=is_winner)
logger.info(
- "Contest text attempt: user %s, round %s, answer '%s', winner %s",
- user_id,
- round_obj.id,
- text_answer,
- is_winner,
+ "Contest text attempt: user , round , answer '', winner",
+ user_id=user_id,
+ round_obj_id=round_obj.id,
+ text_answer=text_answer,
+ is_winner=is_winner,
)
if is_winner:
diff --git a/app/services/daily_subscription_service.py b/app/services/daily_subscription_service.py
index 1732f61e..80134753 100644
--- a/app/services/daily_subscription_service.py
+++ b/app/services/daily_subscription_service.py
@@ -5,9 +5,9 @@
"""
import asyncio
-import logging
from datetime import datetime
+import structlog
from aiogram import Bot
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -29,7 +29,7 @@ from app.services.notification_delivery_service import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class DailySubscriptionService:
@@ -84,15 +84,20 @@ class DailySubscriptionService:
elif result == 'error':
stats['errors'] += 1
except Exception as e:
- logger.error(f'Ошибка обработки суточной подписки {subscription.id}: {e}', exc_info=True)
+ logger.error(
+ 'Ошибка обработки суточной подписки',
+ subscription_id=subscription.id,
+ error=e,
+ exc_info=True,
+ )
stats['errors'] += 1
await db.commit()
except Exception as e:
- logger.error(f'Ошибка при обработке подписок: {e}', exc_info=True)
+ logger.error('Ошибка при обработке подписок', error=e, exc_info=True)
await db.rollback()
except Exception as e:
- logger.error(f'Ошибка при получении подписок для списания: {e}', exc_info=True)
+ logger.error('Ошибка при получении подписок для списания', error=e, exc_info=True)
return stats
@@ -108,17 +113,17 @@ class DailySubscriptionService:
user = await get_user_by_id(db, subscription.user_id)
if not user:
- logger.warning(f'Пользователь не найден для подписки {subscription.id}')
+ logger.warning('Пользователь не найден для подписки', subscription_id=subscription.id)
return 'error'
tariff = subscription.tariff
if not tariff:
- logger.warning(f'Тариф не найден для подписки {subscription.id}')
+ logger.warning('Тариф не найден для подписки', subscription_id=subscription.id)
return 'error'
daily_price = tariff.daily_price_kopeks
if daily_price <= 0:
- logger.warning(f'Некорректная суточная цена для тарифа {tariff.id}')
+ logger.warning('Некорректная суточная цена для тарифа', tariff_id=tariff.id)
return 'error'
# Проверяем баланс
@@ -131,8 +136,10 @@ class DailySubscriptionService:
await self._notify_insufficient_balance(user, subscription, daily_price)
logger.info(
- f'Подписка {subscription.id} приостановлена: недостаточно средств '
- f'(баланс: {user.balance_kopeks}, требуется: {daily_price})'
+ 'Подписка приостановлена: недостаточно средств (баланс: требуется: )',
+ subscription_id=subscription.id,
+ balance_kopeks=user.balance_kopeks,
+ daily_price=daily_price,
)
return 'suspended'
@@ -148,7 +155,7 @@ class DailySubscriptionService:
)
if not deducted:
- logger.warning(f'Не удалось списать средства для подписки {subscription.id}')
+ logger.warning('Не удалось списать средства для подписки', subscription_id=subscription.id)
return 'error'
# Создаём транзакцию
@@ -166,8 +173,10 @@ class DailySubscriptionService:
user_id_display = user.telegram_id or user.email or f'#{user.id}'
logger.info(
- f'✅ Суточное списание: подписка {subscription.id}, '
- f'сумма {daily_price} коп., пользователь {user_id_display}'
+ '✅ Суточное списание: подписка сумма коп., пользователь',
+ subscription_id=subscription.id,
+ daily_price=daily_price,
+ user_id_display=user_id_display,
)
# Синхронизируем с Remnawave (обновляем срок подписки)
@@ -182,7 +191,7 @@ class DailySubscriptionService:
reset_reason=None,
)
except Exception as e:
- logger.warning(f'Не удалось обновить Remnawave: {e}')
+ logger.warning('Не удалось обновить Remnawave', error=e)
# Уведомляем пользователя
if self._bot:
@@ -191,7 +200,9 @@ class DailySubscriptionService:
return 'charged'
except Exception as e:
- logger.error(f'Ошибка при списании средств для подписки {subscription.id}: {e}', exc_info=True)
+ logger.error(
+ 'Ошибка при списании средств для подписки', subscription_id=subscription.id, error=e, exc_info=True
+ )
return 'error'
async def _notify_daily_charge(self, user, subscription, amount_kopeks: int):
@@ -217,7 +228,7 @@ class DailySubscriptionService:
telegram_message=message,
)
except Exception as e:
- logger.warning(f'Не удалось отправить уведомление о списании: {e}')
+ logger.warning('Не удалось отправить уведомление о списании', error=e)
async def _notify_insufficient_balance(self, user, subscription, required_amount: int):
"""Уведомляет пользователя о недостатке средств."""
@@ -258,7 +269,7 @@ class DailySubscriptionService:
telegram_markup=keyboard,
)
except Exception as e:
- logger.warning(f'Не удалось отправить уведомление о недостатке средств: {e}')
+ logger.warning('Не удалось отправить уведомление о недостатке средств', error=e)
async def process_traffic_resets(self) -> dict:
"""
@@ -298,15 +309,20 @@ class DailySubscriptionService:
await self._reset_subscription_traffic(db, subscription_id, purchases)
stats['reset'] += len(purchases)
except Exception as e:
- logger.error(f'Ошибка сброса трафика подписки {subscription_id}: {e}', exc_info=True)
+ logger.error(
+ 'Ошибка сброса трафика подписки',
+ subscription_id=subscription_id,
+ error=e,
+ exc_info=True,
+ )
stats['errors'] += 1
await db.commit()
except Exception as e:
- logger.error(f'Ошибка при обработке сброса трафика: {e}', exc_info=True)
+ logger.error('Ошибка при обработке сброса трафика', error=e, exc_info=True)
await db.rollback()
except Exception as e:
- logger.error(f'Ошибка при получении подписок для сброса трафика: {e}', exc_info=True)
+ logger.error('Ошибка при получении подписок для сброса трафика', error=e, exc_info=True)
return stats
@@ -330,9 +346,11 @@ class DailySubscriptionService:
# КРИТИЧЕСКАЯ ПРОВЕРКА: защита от некорректных данных
if total_expired_gb > old_purchased:
logger.error(
- f'⚠️ ОШИБКА ДАННЫХ: подписка {subscription.id}, '
- f'истекает {total_expired_gb} ГБ, но purchased_traffic_gb = {old_purchased} ГБ. '
- f'Сбрасываем только {old_purchased} ГБ.'
+ '⚠️ ОШИБКА ДАННЫХ: подписка истекает ГБ, но purchased_traffic_gb ГБ. Сбрасываем только ГБ.',
+ subscription_id=subscription.id,
+ total_expired_gb=total_expired_gb,
+ old_purchased=old_purchased,
+ old_purchased_2=old_purchased,
)
total_expired_gb = old_purchased
@@ -349,8 +367,10 @@ class DailySubscriptionService:
# Проверяем, что базовый лимит не отрицательный
if base_limit < 0:
logger.warning(
- f'⚠️ Базовый лимит отрицательный для подписки {subscription.id}: {base_limit} ГБ. '
- f'Используем лимит из тарифа: {tariff_base_limit} ГБ'
+ '⚠️ Базовый лимит отрицательный для подписки ГБ. Используем лимит из тарифа: ГБ',
+ subscription_id=subscription.id,
+ base_limit=base_limit,
+ tariff_base_limit=tariff_base_limit,
)
base_limit = tariff_base_limit
@@ -368,8 +388,9 @@ class DailySubscriptionService:
# Двойная защита: новый лимит не может быть меньше базового
if new_limit < base_limit:
logger.error(
- f'⚠️ КРИТИЧЕСКАЯ ОШИБКА: новый лимит ({new_limit} ГБ) меньше базового ({base_limit} ГБ). '
- f'Устанавливаем базовый лимит.'
+ '⚠️ КРИТИЧЕСКАЯ ОШИБКА: новый лимит ( ГБ) меньше базового ( ГБ). Устанавливаем базовый лимит.',
+ new_limit=new_limit,
+ base_limit=base_limit,
)
new_limit = base_limit
new_purchased = 0
@@ -401,10 +422,16 @@ class DailySubscriptionService:
await db.commit()
logger.info(
- f'🔄 Сброс истекших докупок: подписка {subscription.id}, '
- f'было {old_limit} ГБ (базовый: {base_limit} ГБ, докуплено: {old_purchased} ГБ), '
- f'стало {subscription.traffic_limit_gb} ГБ (базовый: {base_limit} ГБ, докуплено: {new_purchased} ГБ), '
- f'убрано {total_expired_gb} ГБ из {len(expired_purchases)} покупок'
+ '🔄 Сброс истекших докупок: подписка было ГБ (базовый: ГБ, докуплено: ГБ), стало ГБ (базовый: ГБ, докуплено: ГБ), убрано ГБ из покупок',
+ subscription_id=subscription.id,
+ old_limit=old_limit,
+ base_limit=base_limit,
+ old_purchased=old_purchased,
+ traffic_limit_gb=subscription.traffic_limit_gb,
+ base_limit_2=base_limit,
+ new_purchased=new_purchased,
+ total_expired_gb=total_expired_gb,
+ expired_purchases_count=len(expired_purchases),
)
# Синхронизируем с RemnaWave
@@ -414,7 +441,7 @@ class DailySubscriptionService:
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
except Exception as e:
- logger.warning(f'Не удалось синхронизировать с RemnaWave после сброса трафика: {e}')
+ logger.warning('Не удалось синхронизировать с RemnaWave после сброса трафика', error=e)
# Уведомляем пользователя
if self._bot and subscription.user_id:
@@ -447,14 +474,14 @@ class DailySubscriptionService:
telegram_message=message,
)
except Exception as e:
- logger.warning(f'Не удалось отправить уведомление о сбросе трафика: {e}')
+ logger.warning('Не удалось отправить уведомление о сбросе трафика', error=e)
async def start_monitoring(self):
"""Запускает периодическую проверку суточных подписок и сброса трафика."""
self._running = True
interval_minutes = self.get_check_interval_minutes()
- logger.info(f'🔄 Запуск сервиса суточных подписок (интервал: {interval_minutes} мин)')
+ logger.info('🔄 Запуск сервиса суточных подписок (интервал: мин)', interval_minutes=interval_minutes)
while self._running:
try:
@@ -463,20 +490,24 @@ class DailySubscriptionService:
if stats['charged'] > 0 or stats['suspended'] > 0:
logger.info(
- f'📊 Суточные списания: проверено={stats["checked"]}, '
- f'списано={stats["charged"]}, приостановлено={stats["suspended"]}, '
- f'ошибок={stats["errors"]}'
+ '📊 Суточные списания: проверено=, списано=, приостановлено=, ошибок',
+ stats=stats['checked'],
+ stats_2=stats['charged'],
+ stats_3=stats['suspended'],
+ stats_4=stats['errors'],
)
# Обработка сброса докупленного трафика
traffic_stats = await self.process_traffic_resets()
if traffic_stats['reset'] > 0:
logger.info(
- f'📊 Сброс трафика: проверено={traffic_stats["checked"]}, '
- f'сброшено={traffic_stats["reset"]}, ошибок={traffic_stats["errors"]}'
+ '📊 Сброс трафика: проверено=, сброшено=, ошибок',
+ traffic_stats=traffic_stats['checked'],
+ traffic_stats_2=traffic_stats['reset'],
+ traffic_stats_3=traffic_stats['errors'],
)
except Exception as e:
- logger.error(f'Ошибка в цикле проверки суточных подписок: {e}', exc_info=True)
+ logger.error('Ошибка в цикле проверки суточных подписок', error=e, exc_info=True)
await asyncio.sleep(interval_minutes * 60)
diff --git a/app/services/disposable_email_service.py b/app/services/disposable_email_service.py
index 3bb9a370..ec08640d 100644
--- a/app/services/disposable_email_service.py
+++ b/app/services/disposable_email_service.py
@@ -1,15 +1,15 @@
"""Service for blocking disposable/temporary email domains."""
import asyncio
-import logging
from datetime import UTC, datetime
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class DisposableEmailService:
@@ -34,7 +34,7 @@ class DisposableEmailService:
"""Load domains and start periodic refresh task."""
await self._update_domains()
self._task = asyncio.create_task(self._periodic_loop())
- logger.info('DisposableEmailService started (%d domains loaded)', self._domain_count)
+ logger.info('DisposableEmailService started (domains loaded)', domain_count=self._domain_count)
async def stop(self) -> None:
"""Cancel periodic refresh task."""
@@ -52,10 +52,7 @@ class DisposableEmailService:
try:
async with aiohttp.ClientSession() as session, session.get(self.DOMAINS_URL) as resp:
if resp.status != 200:
- logger.error(
- 'Failed to fetch disposable domains: HTTP %d',
- resp.status,
- )
+ logger.error('Failed to fetch disposable domains: HTTP', resp_status=resp.status)
return
text = await resp.text()
@@ -67,7 +64,7 @@ class DisposableEmailService:
self._domains = domains
self._domain_count = len(domains)
self._last_updated = datetime.now(UTC)
- logger.info('Disposable email domains updated: %d domains', self._domain_count)
+ logger.info('Disposable email domains updated: domains', domain_count=self._domain_count)
except Exception:
logger.exception('Error updating disposable email domains')
diff --git a/app/services/event_emitter.py b/app/services/event_emitter.py
index 83a0a530..3e254340 100644
--- a/app/services/event_emitter.py
+++ b/app/services/event_emitter.py
@@ -2,17 +2,17 @@ from __future__ import annotations
import asyncio
import json
-import logging
from collections.abc import Callable
from datetime import datetime
from typing import Any
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.webhook_service import webhook_service
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class EventEmitter:
@@ -39,12 +39,16 @@ class EventEmitter:
def register_websocket(self, websocket: Any) -> None:
"""Зарегистрировать WebSocket подключение."""
self._websocket_connections.add(websocket)
- logger.debug('WebSocket connection registered. Total: %d', len(self._websocket_connections))
+ logger.debug(
+ 'WebSocket connection registered. Total', websocket_connections_count=len(self._websocket_connections)
+ )
def unregister_websocket(self, websocket: Any) -> None:
"""Отменить регистрацию WebSocket подключения."""
self._websocket_connections.discard(websocket)
- logger.debug('WebSocket connection unregistered. Total: %d', len(self._websocket_connections))
+ logger.debug(
+ 'WebSocket connection unregistered. Total', websocket_connections_count=len(self._websocket_connections)
+ )
async def emit(
self,
@@ -68,7 +72,7 @@ class EventEmitter:
else:
callback(event_data)
except Exception as error:
- logger.exception('Error in event listener for %s: %s', event_type, error)
+ logger.exception('Error in event listener for', event_type=event_type, error=error)
# Отправляем через WebSocket
await self._broadcast_to_websockets(event_data)
@@ -89,7 +93,7 @@ class EventEmitter:
try:
await ws.send_text(message)
except Exception as error:
- logger.warning('Failed to send WebSocket message: %s', error)
+ logger.warning('Failed to send WebSocket message', error=error)
disconnected.add(ws)
# Удаляем отключенные соединения
diff --git a/app/services/external_admin_service.py b/app/services/external_admin_service.py
index 73cc950b..12a7b797 100644
--- a/app/services/external_admin_service.py
+++ b/app/services/external_admin_service.py
@@ -2,8 +2,7 @@
from __future__ import annotations
-import logging
-
+import structlog
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
@@ -16,7 +15,7 @@ from app.services.system_settings_service import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def ensure_external_admin_token(
@@ -42,7 +41,7 @@ async def ensure_external_admin_token(
try:
token = settings.build_external_admin_token(normalized_username)
except Exception as error: # pragma: no cover - защитный блок
- logger.error('❌ Ошибка генерации токена внешней админки: %s', error)
+ logger.error('❌ Ошибка генерации токена внешней админки', error=error)
return None
try:
@@ -62,8 +61,8 @@ async def ensure_external_admin_token(
existing_bot_id = int(existing_bot_id_raw)
except (TypeError, ValueError): # pragma: no cover - защита от мусорных значений
logger.warning(
- '⚠️ Не удалось разобрать сохраненный идентификатор бота внешней админки: %s',
- existing_bot_id_raw,
+ '⚠️ Не удалось разобрать сохраненный идентификатор бота внешней админки',
+ existing_bot_id_raw=existing_bot_id_raw,
)
if existing_token == token and existing_bot_id == bot_id:
@@ -75,9 +74,9 @@ async def ensure_external_admin_token(
if existing_bot_id is not None and bot_id is not None and existing_bot_id != bot_id:
logger.error(
- '❌ Обнаружено несовпадение ID бота для токена внешней админки: сохранен %s, текущий %s',
- existing_bot_id,
- bot_id,
+ '❌ Обнаружено несовпадение ID бота для токена внешней админки: сохранен , текущий',
+ existing_bot_id=existing_bot_id,
+ bot_id=bot_id,
)
try:
@@ -98,8 +97,8 @@ async def ensure_external_admin_token(
except Exception as cleanup_error: # pragma: no cover - защитный блок
await session.rollback()
logger.error(
- '❌ Не удалось очистить токен внешней админки после обнаружения подмены: %s',
- cleanup_error,
+ '❌ Не удалось очистить токен внешней админки после обнаружения подмены',
+ cleanup_error=cleanup_error,
)
finally:
settings.EXTERNAL_ADMIN_TOKEN = None
@@ -133,10 +132,7 @@ async def ensure_external_admin_token(
force=True,
)
await session.commit()
- logger.info(
- '✅ Токен внешней админки синхронизирован для @%s',
- normalized_username,
- )
+ logger.info('✅ Токен внешней админки синхронизирован для @', normalized_username=normalized_username)
except ReadOnlySettingError: # pragma: no cover - force=True предотвращает исключение
await session.rollback()
logger.warning(
@@ -146,5 +142,5 @@ async def ensure_external_admin_token(
return token
except SQLAlchemyError as error:
- logger.error('❌ Ошибка сохранения токена внешней админки: %s', error)
+ logger.error('❌ Ошибка сохранения токена внешней админки', error=error)
return None
diff --git a/app/services/faq_service.py b/app/services/faq_service.py
index 4e8b712f..8abea999 100644
--- a/app/services/faq_service.py
+++ b/app/services/faq_service.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -16,7 +15,7 @@ from app.database.crud.faq import (
from app.database.models import FaqPage, FaqSetting
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class FaqService:
diff --git a/app/services/freekassa_service.py b/app/services/freekassa_service.py
index 1b90fe1a..e091c1da 100644
--- a/app/services/freekassa_service.py
+++ b/app/services/freekassa_service.py
@@ -4,17 +4,17 @@ import asyncio
import hashlib
import hmac
import json
-import logging
import time
import urllib.request
from typing import Any
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Кэш для публичного IP
_cached_public_ip: str | None = None
@@ -71,15 +71,15 @@ async def get_public_ip() -> str:
# Простая валидация IPv4
if ip and len(ip.split('.')) == 4:
_cached_public_ip = ip
- logger.info(f'Определён публичный IP сервера: {ip}')
+ logger.info('Определён публичный IP сервера', ip=ip)
return ip
except Exception as e:
- logger.debug(f'Не удалось получить IP от {service_url}: {e}')
+ logger.debug('Не удалось получить IP от', service_url=service_url, error=e)
continue
# Fallback на известный рабочий IP если ничего не получилось
fallback_ip = '185.92.183.173'
- logger.warning(f'Не удалось определить публичный IP, используем fallback: {fallback_ip}')
+ logger.warning('Не удалось определить публичный IP, используем fallback', fallback_ip=fallback_ip)
_cached_public_ip = fallback_ip
return fallback_ip
@@ -206,7 +206,7 @@ class FreekassaService:
# Генерация подписи
params['signature'] = self._generate_api_signature(params)
- logger.info(f'Freekassa synchronous build_payment_url for 44: {params}')
+ logger.info('Freekassa synchronous build_payment_url for 44', params=params)
data_json = json.dumps(params).encode('utf-8')
req = urllib.request.Request(
@@ -218,7 +218,7 @@ class FreekassaService:
data = json.loads(resp_body)
if data.get('type') == 'error':
- logger.error(f'Freekassa build_payment_url error: {data}')
+ logger.error('Freekassa build_payment_url error', data=data)
# Fallback to standard flow if error? Or raise?
# User wants it to work. Raise to see error is safer.
# raise Exception(f"Freekassa API Error: {data.get('message')}")
@@ -228,7 +228,7 @@ class FreekassaService:
if data.get('location'):
return data.get('location')
except Exception as e:
- logger.error(f'Failed to create order 44 via sync API: {e}')
+ logger.error('Failed to create order 44 via sync API', error=e)
# Если не получилось, попробуем сгенерировать обычную ссылку как fallback
signature = self.generate_form_signature(final_amount, currency, order_id)
@@ -297,7 +297,7 @@ class FreekassaService:
# Генерируем подпись HMAC-SHA256
params['signature'] = self._generate_api_signature(params)
- logger.info(f'Freekassa API create_order params: {params}')
+ logger.info('Freekassa API create_order params', params=params)
try:
async with (
@@ -310,19 +310,19 @@ class FreekassaService:
) as response,
):
text = await response.text()
- logger.info(f'Freekassa API response: {text}')
+ logger.info('Freekassa API response', text=text)
data = await response.json()
# Проверяем на ошибку - API может вернуть error или type=error
error_msg = data.get('error') or data.get('message')
if response.status != 200 or data.get('type') == 'error' or error_msg:
- logger.error(f'Freekassa create_order error: {data}')
+ logger.error('Freekassa create_order error', data=data)
raise Exception(f'Freekassa API error: {error_msg or "Unknown error"}')
return data
except aiohttp.ClientError as e:
- logger.exception(f'Freekassa API connection error: {e}')
+ logger.exception('Freekassa API connection error', error=e)
raise
async def create_order_and_get_url(
@@ -363,7 +363,7 @@ class FreekassaService:
}
params['signature'] = self._generate_api_signature(params)
- logger.debug(f'Freekassa get_order_status params: {params}')
+ logger.debug('Freekassa get_order_status params', params=params)
try:
async with (
@@ -376,10 +376,10 @@ class FreekassaService:
) as response,
):
text = await response.text()
- logger.debug(f'Freekassa get_order_status response: {text}')
+ logger.debug('Freekassa get_order_status response', text=text)
return await response.json()
except aiohttp.ClientError as e:
- logger.exception(f'Freekassa API connection error: {e}')
+ logger.exception('Freekassa API connection error', error=e)
raise
async def get_balance(self) -> dict[str, Any]:
@@ -402,7 +402,7 @@ class FreekassaService:
):
return await response.json()
except aiohttp.ClientError as e:
- logger.exception(f'Freekassa API connection error: {e}')
+ logger.exception('Freekassa API connection error', error=e)
raise
async def get_payment_systems(self) -> dict[str, Any]:
@@ -425,7 +425,7 @@ class FreekassaService:
):
return await response.json()
except aiohttp.ClientError as e:
- logger.exception(f'Freekassa API connection error: {e}')
+ logger.exception('Freekassa API connection error', error=e)
raise
diff --git a/app/services/kassa_ai_service.py b/app/services/kassa_ai_service.py
index b219791f..3b37f838 100644
--- a/app/services/kassa_ai_service.py
+++ b/app/services/kassa_ai_service.py
@@ -3,16 +3,16 @@
import asyncio
import hashlib
import hmac
-import logging
import time
from typing import Any
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Кэш для публичного IP
_cached_public_ip: str | None = None
@@ -56,14 +56,14 @@ async def get_public_ip() -> str:
ip = (await response.text()).strip()
if ip and len(ip.split('.')) == 4:
_cached_public_ip = ip
- logger.info(f'KassaAI: определён публичный IP сервера: {ip}')
+ logger.info('KassaAI: определён публичный IP сервера', ip=ip)
return ip
except Exception as e:
- logger.debug(f'KassaAI: не удалось получить IP от {service_url}: {e}')
+ logger.debug('KassaAI: не удалось получить IP от', service_url=service_url, error=e)
continue
fallback_ip = '127.0.0.1'
- logger.warning(f'KassaAI: не удалось определить публичный IP, используем fallback: {fallback_ip}')
+ logger.warning('KassaAI: не удалось определить публичный IP, используем fallback', fallback_ip=fallback_ip)
_cached_public_ip = fallback_ip
return fallback_ip
@@ -122,7 +122,7 @@ class KassaAiService:
return expected_sign.lower() == sign.lower()
except Exception as e:
- logger.error(f'KassaAI webhook verify error: {e}')
+ logger.error('KassaAI webhook verify error', error=e)
return False
async def create_order(
@@ -170,7 +170,11 @@ class KassaAiService:
params['signature'] = self._generate_hmac_signature(params)
logger.info(
- f'KassaAI API create_order: shop_id={self.shop_id}, order_id={order_id}, amount={final_amount}, ps_id={ps_id}'
+ 'KassaAI API create_order: shop_id=, order_id=, amount=, ps_id',
+ shop_id=self.shop_id,
+ order_id=order_id,
+ final_amount=final_amount,
+ ps_id=ps_id,
)
try:
@@ -184,14 +188,14 @@ class KassaAiService:
) as response,
):
text = await response.text()
- logger.info(f'KassaAI API response: {text}')
+ logger.info('KassaAI API response', text=text)
data = await response.json()
# Проверяем на ошибку
if data.get('type') == 'error':
error_msg = data.get('error') or data.get('message') or 'Unknown error'
- logger.error(f'KassaAI create_order error: {error_msg}')
+ logger.error('KassaAI create_order error', error_msg=error_msg)
raise Exception(f'KassaAI API error: {error_msg}')
if data.get('type') == 'success':
@@ -202,11 +206,11 @@ class KassaAiService:
}
# Неизвестный формат ответа
- logger.error(f'KassaAI unexpected response: {data}')
+ logger.error('KassaAI unexpected response', data=data)
raise Exception('KassaAI unexpected response format')
except aiohttp.ClientError as e:
- logger.exception(f'KassaAI API connection error: {e}')
+ logger.exception('KassaAI API connection error', error=e)
raise
async def create_order_and_get_url(
@@ -246,7 +250,7 @@ class KassaAiService:
}
params['signature'] = self._generate_hmac_signature(params)
- logger.info(f'KassaAI get_order_status: order_id={order_id}')
+ logger.info('KassaAI get_order_status: order_id', order_id=order_id)
try:
async with (
@@ -259,10 +263,10 @@ class KassaAiService:
) as response,
):
text = await response.text()
- logger.info(f'KassaAI get_order_status response: {text}')
+ logger.info('KassaAI get_order_status response', text=text)
return await response.json()
except aiohttp.ClientError as e:
- logger.exception(f'KassaAI API connection error: {e}')
+ logger.exception('KassaAI API connection error', error=e)
raise
async def get_balance(self) -> dict[str, Any]:
@@ -285,7 +289,7 @@ class KassaAiService:
):
return await response.json()
except aiohttp.ClientError as e:
- logger.exception(f'KassaAI API connection error: {e}')
+ logger.exception('KassaAI API connection error', error=e)
raise
diff --git a/app/services/log_rotation_service.py b/app/services/log_rotation_service.py
index 5331f9d9..c956be42 100644
--- a/app/services/log_rotation_service.py
+++ b/app/services/log_rotation_service.py
@@ -18,6 +18,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
+import structlog
from aiogram import Bot
from aiogram.types import FSInputFile
@@ -25,7 +26,7 @@ from app.config import settings
from app.utils.timezone import get_local_timezone
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@dataclass
@@ -113,9 +114,9 @@ class LogRotationService:
if wait_seconds > 0:
logger.info(
- 'Следующая ротация логов: %s (через %.1f часов)',
- next_rotation.strftime('%Y-%m-%d %H:%M'),
- wait_seconds / 3600,
+ 'Следующая ротация логов: (через часов)',
+ next_rotation=next_rotation.strftime('%Y-%m-%d %H:%M'),
+ wait_seconds=wait_seconds / 3600,
)
try:
await asyncio.sleep(wait_seconds)
@@ -135,7 +136,7 @@ class LogRotationService:
hours, minutes = map(int, time_str.split(':'))
except ValueError:
hours, minutes = 0, 0
- logger.warning("Некорректное LOG_ROTATION_TIME='%s', используем 00:00", time_str)
+ logger.warning("Некорректное LOG_ROTATION_TIME='', используем 00:00", time_str=time_str)
next_rotation = now.replace(hour=hours, minute=minutes, second=0, microsecond=0)
@@ -234,11 +235,11 @@ class LogRotationService:
tar.add(file_path, arcname=arcname)
await asyncio.to_thread(_create_tar)
- logger.debug('Создан архив: %s', archive_path)
+ logger.debug('Создан архив', archive_path=archive_path)
return archive_path
except Exception as error:
- logger.error('Ошибка создания архива: %s', error)
+ logger.error('Ошибка создания архива', error=error)
return None
async def _cleanup_old_archives(self) -> None:
@@ -267,7 +268,7 @@ class LogRotationService:
if file_date < cutoff_date:
archive_file.unlink()
- logger.info('Удален старый архив логов: %s', archive_file.name)
+ logger.info('Удален старый архив логов', archive_file_name=archive_file.name)
except ValueError:
# Пропускаем файлы с некорректным форматом имени
pass
@@ -305,10 +306,10 @@ class LogRotationService:
send_kwargs['message_thread_id'] = topic_id
await self.bot.send_document(**send_kwargs)
- logger.info('Архив логов отправлен: %s', archive_path.name)
+ logger.info('Архив логов отправлен', archive_path_name=archive_path.name)
except Exception as error:
- logger.error('Ошибка отправки архива %s: %s', archive_path.name, error)
+ logger.error('Ошибка отправки архива', archive_path_name=archive_path.name, error=error)
# === Ручные операции ===
diff --git a/app/services/maintenance_service.py b/app/services/maintenance_service.py
index 58a72e5f..987c2699 100644
--- a/app/services/maintenance_service.py
+++ b/app/services/maintenance_service.py
@@ -1,16 +1,17 @@
import asyncio
-import logging
from dataclasses import dataclass
from datetime import datetime
from typing import Any
+import structlog
+
from app.config import settings
from app.external.remnawave_api import RemnaWaveAPI, test_api_connection
from app.utils.cache import cache
from app.utils.timezone import format_local_datetime
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@dataclass
@@ -81,7 +82,7 @@ class MaintenanceService:
return await notification_service._send_message(formatted_message)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления через AdminNotificationService: {e}')
+ logger.error('Ошибка отправки уведомления через AdminNotificationService', error=e)
return False
async def _notify_admins(self, message: str, alert_type: str = 'info'):
@@ -119,10 +120,10 @@ class MaintenanceService:
await asyncio.sleep(0.1)
except Exception as e:
- logger.error(f'Ошибка отправки уведомления админу {admin_id}: {e}')
+ logger.error('Ошибка отправки уведомления админу', admin_id=admin_id, error=e)
if success_count > 0:
- logger.info(f'Уведомление отправлено {success_count} администраторам')
+ logger.info('Уведомление отправлено администраторам', success_count=success_count)
await cache.set(cache_key, True, expire=300)
else:
logger.error('Не удалось отправить уведомления ни одному администратору')
@@ -151,11 +152,11 @@ class MaintenanceService:
await self._notify_admins(notification_msg, 'warning' if auto else 'info')
- logger.warning(f'🔧 Режим техработ ВКЛЮЧЕН. Причина: {self._status.reason}')
+ logger.warning('🔧 Режим техработ ВКЛЮЧЕН. Причина', reason=self._status.reason)
return True
except Exception as e:
- logger.error(f'Ошибка включения режима техработ: {e}')
+ logger.error('Ошибка включения режима техработ', error=e)
return False
async def disable_maintenance(self) -> bool:
@@ -201,7 +202,7 @@ class MaintenanceService:
return True
except Exception as e:
- logger.error(f'Ошибка выключения режима техработ: {e}')
+ logger.error('Ошибка выключения режима техработ', error=e)
return False
async def start_monitoring(self) -> bool:
@@ -214,9 +215,9 @@ class MaintenanceService:
self._check_task = asyncio.create_task(self._monitoring_loop())
logger.info(
- '🔄 Запущен мониторинг API Remnawave (интервал: %sс, попыток: %s)',
- settings.get_maintenance_check_interval(),
- settings.get_maintenance_retry_attempts(),
+ '🔄 Запущен мониторинг API Remnawave (интервал: с, попыток:)',
+ get_maintenance_check_interval=settings.get_maintenance_check_interval(),
+ get_maintenance_retry_attempts=settings.get_maintenance_retry_attempts(),
)
# Сообщение о запуске мониторинга убрано - теперь используется
@@ -225,7 +226,7 @@ class MaintenanceService:
return True
except Exception as e:
- logger.error(f'Ошибка запуска мониторинга: {e}')
+ logger.error('Ошибка запуска мониторинга', error=e)
return False
async def stop_monitoring(self) -> bool:
@@ -242,7 +243,7 @@ class MaintenanceService:
return True
except Exception as e:
- logger.error(f'Ошибка остановки мониторинга: {e}')
+ logger.error('Ошибка остановки мониторинга', error=e)
return False
async def check_api_status(self) -> bool:
@@ -292,7 +293,7 @@ class MaintenanceService:
if is_connected:
if attempt > 1:
- logger.info('API Remnawave ответило с %s попытки', attempt)
+ logger.info('API Remnawave ответило с попытки', attempt=attempt)
if not self._status.api_status:
recovery_time = format_local_datetime(self._status.last_check, '%H:%M:%S %Z')
@@ -317,11 +318,7 @@ API снова отвечает на запросы.""",
return True
if attempt < attempts:
- logger.warning(
- 'API Remnawave недоступно (попытка %s/%s)',
- attempt,
- attempts,
- )
+ logger.warning('API Remnawave недоступно (попытка /)', attempt=attempt, attempts=attempts)
await asyncio.sleep(1)
was_available = self._status.api_status
@@ -356,7 +353,7 @@ API снова отвечает на запросы.""",
return False
except Exception as e:
- logger.error(f'Ошибка проверки API: {e}')
+ logger.error('Ошибка проверки API', error=e)
if self._status.api_status:
error_time = format_local_datetime(datetime.utcnow(), '%H:%M:%S %Z')
@@ -387,7 +384,7 @@ API снова отвечает на запросы.""",
logger.info('Мониторинг отменен')
break
except Exception as e:
- logger.error(f'Ошибка в цикле мониторинга: {e}')
+ logger.error('Ошибка в цикле мониторинга', error=e)
await asyncio.sleep(30)
async def _save_status_to_cache(self):
@@ -404,7 +401,7 @@ API снова отвечает на запросы.""",
await cache.set('maintenance_status', status_data, expire=3600)
except Exception as e:
- logger.error(f'Ошибка сохранения состояния в кеш: {e}')
+ logger.error('Ошибка сохранения состояния в кеш', error=e)
async def _load_status_from_cache(self):
try:
@@ -423,10 +420,10 @@ API снова отвечает на запросы.""",
if status_data.get('last_check'):
self._status.last_check = datetime.fromisoformat(status_data['last_check'])
- logger.info(f'🔥 Состояние техработ загружено из кеша: активен={self._status.is_active}')
+ logger.info('🔥 Состояние техработ загружено из кеша: активен', is_active=self._status.is_active)
except Exception as e:
- logger.error(f'Ошибка загрузки состояния из кеша: {e}')
+ logger.error('Ошибка загрузки состояния из кеша', error=e)
def get_status_info(self) -> dict[str, Any]:
return {
@@ -488,11 +485,11 @@ API снова отвечает на запросы.""",
alert_type = 'error' if status in ['offline', 'error'] else 'info'
await self._notify_admins(message, alert_type)
- logger.info(f'Отправлено уведомление о статусе Remnawave: {status}')
+ logger.info('Отправлено уведомление о статусе Remnawave', status=status)
return True
except Exception as e:
- logger.error(f'Ошибка отправки уведомления о статусе Remnawave: {e}')
+ logger.error('Ошибка отправки уведомления о статусе Remnawave', error=e)
return False
diff --git a/app/services/menu_layout/service.py b/app/services/menu_layout/service.py
index b85b0be8..2b184e4f 100644
--- a/app/services/menu_layout/service.py
+++ b/app/services/menu_layout/service.py
@@ -5,10 +5,10 @@ from __future__ import annotations
import asyncio
import copy
import json
-import logging
from datetime import UTC, datetime
from typing import Any
+import structlog
from aiogram import types
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import select
@@ -31,7 +31,7 @@ from .history_service import MenuLayoutHistoryService
from .stats_service import MenuLayoutStatsService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class MenuLayoutService:
@@ -289,7 +289,9 @@ class MenuLayoutService:
for key in buttons.keys():
if key == 'connect' or buttons[key].get('builtin_id') == 'connect':
actual_button_id = key
- logger.info(f"🔗 Найдена кнопка connect по ID '{button_id}' -> '{actual_button_id}'")
+ logger.info(
+ '🔗 Найдена кнопка connect по ID', button_id=button_id, actual_button_id=actual_button_id
+ )
break
else:
# Если не нашли, пробуем найти по builtin_id
@@ -300,7 +302,9 @@ class MenuLayoutService:
):
actual_button_id = key
logger.info(
- f"🔗 Найдена кнопка connect по builtin_id '{button_id}' -> '{actual_button_id}'"
+ '🔗 Найдена кнопка connect по builtin_id',
+ button_id=button_id,
+ actual_button_id=actual_button_id,
)
break
else:
@@ -314,10 +318,11 @@ class MenuLayoutService:
# Логирование для отладки
if 'connect' in actual_button_id.lower() or button.get('builtin_id') == 'connect':
logger.info(
- f'🔗 Обновление кнопки connect (ID: {actual_button_id}): '
- f'open_mode={updates.get("open_mode")}, '
- f'action={updates.get("action")}, '
- f'webapp_url={updates.get("webapp_url")}'
+ '🔗 Обновление кнопки connect (ID: ): open_mode=, action=, webapp_url',
+ actual_button_id=actual_button_id,
+ get=updates.get('open_mode'),
+ get_2=updates.get('action'),
+ get_3=updates.get('webapp_url'),
)
# Применяем обновления
@@ -1012,10 +1017,12 @@ class MenuLayoutService:
if is_connect_button:
logger.info(
- f'🔗 Построение кнопки connect: '
- f'button_id={effective_button_id}, type={button_type}, '
- f'open_mode={open_mode}, action={action}, '
- f'webapp_url={webapp_url}'
+ '🔗 Построение кнопки connect: button_id=, type=, open_mode=, action=, webapp_url',
+ effective_button_id=effective_button_id,
+ button_type=button_type,
+ open_mode=open_mode,
+ action=action,
+ webapp_url=webapp_url,
)
# Получаем текст
@@ -1054,26 +1061,27 @@ class MenuLayoutService:
subscription_url = get_display_subscription_link(context.subscription)
if subscription_url:
url = subscription_url
- logger.info(f'🔗 Кнопка connect: получен URL из подписки: {url[:50]}...')
+ logger.info('🔗 Кнопка connect: получен URL из подписки: ...', url=url[:50])
# Если все еще нет URL, пробуем использовать настройку MINIAPP_CUSTOM_URL
if not url or not (url.startswith('http://') or url.startswith('https://')):
if settings.MINIAPP_CUSTOM_URL:
url = settings.MINIAPP_CUSTOM_URL
- logger.info(f'🔗 Кнопка connect: использован MINIAPP_CUSTOM_URL: {url[:50]}...')
+ logger.info('🔗 Кнопка connect: использован MINIAPP_CUSTOM_URL: ...', url=url[:50])
# Проверяем, что это действительно URL
if url and (url.startswith('http://') or url.startswith('https://')):
- logger.info(f'🔗 Кнопка connect: open_mode=direct, используем URL: {url[:50]}...')
+ logger.info('🔗 Кнопка connect: open_mode=direct, используем URL: ...', url=url[:50])
return InlineKeyboardButton(text=text, web_app=types.WebAppInfo(url=url))
logger.warning(
- f'🔗 Кнопка connect: open_mode=direct, но URL не найден. '
- f'webapp_url={webapp_url}, action={action}, '
- f'subscription_url={"есть" if context.subscription else "нет"}'
+ '🔗 Кнопка connect: open_mode=direct, но URL не найден. webapp_url=, action=, subscription_url',
+ webapp_url=webapp_url,
+ action=action,
+ value='есть' if context.subscription else 'нет',
)
# Fallback на callback_data
return InlineKeyboardButton(text=text, callback_data=action)
# Стандартный callback_data
- logger.debug(f'Кнопка connect: open_mode={open_mode}, используем callback_data: {action}')
+ logger.debug('Кнопка connect: open_mode=, используем callback_data', open_mode=open_mode, action=action)
return InlineKeyboardButton(text=text, callback_data=action)
# --- Построение клавиатуры ---
diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py
index 3286aaa3..379a7878 100644
--- a/app/services/monitoring_service.py
+++ b/app/services/monitoring_service.py
@@ -1,9 +1,9 @@
import asyncio
-import logging
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
+import structlog
from aiogram.enums import ChatMemberStatus
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramNetworkError
from sqlalchemy import and_, or_, select
@@ -73,7 +73,7 @@ from app.utils.timezone import format_local_datetime
AUTOPAY_INSUFFICIENT_BALANCE_COOLDOWN_SECONDS: int = 21600
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
LOGO_PATH = Path(settings.LOGO_FILE)
@@ -120,9 +120,9 @@ class MonitoringService:
return result
except TelegramBadRequest as exc:
logger.warning(
- 'Не удалось отправить сообщение с логотипом пользователю %s: %s. Отправляем текстовое сообщение.',
- chat_id,
- exc,
+ 'Не удалось отправить сообщение с логотипом пользователю : . Отправляем текстовое сообщение.',
+ chat_id=chat_id,
+ exc=exc,
)
return await self.bot.send_message(
@@ -149,19 +149,12 @@ class MonitoringService:
def _handle_unreachable_user(self, user: User, error: Exception, context: str) -> bool:
if isinstance(error, TelegramForbiddenError):
logger.warning(
- '⚠️ Пользователь %s недоступен (%s): бот заблокирован',
- user.telegram_id,
- context,
+ '⚠️ Пользователь недоступен : бот заблокирован', telegram_id=user.telegram_id, context=context
)
return True
if isinstance(error, TelegramBadRequest) and self._is_unreachable_error(error):
- logger.warning(
- '⚠️ Пользователь %s недоступен (%s): %s',
- user.telegram_id,
- context,
- error,
- )
+ logger.warning('⚠️ Пользователь недоступен', telegram_id=user.telegram_id, context=context, error=error)
return True
return False
@@ -178,7 +171,7 @@ class MonitoringService:
if not self._sla_task or self._sla_task.done():
self._sla_task = asyncio.create_task(self._sla_loop())
except Exception as e:
- logger.error(f'Не удалось запустить SLA-мониторинг: {e}')
+ logger.error('Не удалось запустить SLA-мониторинг', error=e)
while self.is_running:
try:
@@ -186,7 +179,7 @@ class MonitoringService:
await asyncio.sleep(settings.MONITORING_INTERVAL * 60)
except Exception as e:
- logger.error(f'Ошибка в цикле мониторинга: {e}')
+ logger.error('Ошибка в цикле мониторинга', error=e)
await asyncio.sleep(60)
def stop_monitoring(self):
@@ -205,18 +198,20 @@ class MonitoringService:
expired_offers = await deactivate_expired_offers(db)
if expired_offers:
- logger.info(f'🧹 Деактивировано {expired_offers} просроченных скидочных предложений')
+ logger.info('🧹 Деактивировано просроченных скидочных предложений', expired_offers=expired_offers)
expired_active_discounts = await cleanup_expired_promo_offer_discounts(db)
if expired_active_discounts:
logger.info(
- '🧹 Сброшено %s активных скидок промо-предложений с истекшим сроком',
- expired_active_discounts,
+ '🧹 Сброшено активных скидок промо-предложений с истекшим сроком',
+ expired_active_discounts=expired_active_discounts,
)
cleaned_test_access = await promo_offer_service.cleanup_expired_test_access(db)
if cleaned_test_access:
- logger.info(f'🧹 Отозвано {cleaned_test_access} истекших тестовых доступов к сквадам')
+ logger.info(
+ '🧹 Отозвано истекших тестовых доступов к сквадам', cleaned_test_access=cleaned_test_access
+ )
await self._check_expired_subscriptions(db)
await self._check_expiring_subscriptions(db)
@@ -237,7 +232,7 @@ class MonitoringService:
await db.commit()
except Exception as e:
- logger.error(f'Ошибка в цикле мониторинга: {e}')
+ logger.error('Ошибка в цикле мониторинга', error=e)
try:
await self._log_monitoring_event(
db,
@@ -257,7 +252,7 @@ class MonitoringService:
old_count = len(self._notified_users)
self._notified_users.clear()
self._last_cleanup = current_time
- logger.info(f'🧹 Очищен кеш уведомлений ({old_count} записей)')
+ logger.info('🧹 Очищен кеш уведомлений ( записей)', old_count=old_count)
async def _check_expired_subscriptions(self, db: AsyncSession):
try:
@@ -268,8 +263,7 @@ class MonitoringService:
for subscription in expired_subscriptions:
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск expire подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск expire подписки : обновлена вебхуком недавно', subscription_id=subscription.id
)
continue
@@ -281,7 +275,9 @@ class MonitoringService:
if user and self.bot:
await self._send_subscription_expired_notification(user)
- logger.info(f"🔴 Подписка пользователя {subscription.user_id} истекла и статус изменен на 'expired'")
+ logger.info(
+ "🔴 Подписка пользователя истекла и статус изменен на 'expired'", user_id=subscription.user_id
+ )
if expired_subscriptions:
await self._log_monitoring_event(
@@ -292,7 +288,7 @@ class MonitoringService:
)
except Exception as e:
- logger.error(f'Ошибка проверки истёкших подписок: {e}')
+ logger.error('Ошибка проверки истёкших подписок', error=e)
async def update_remnawave_user(self, db: AsyncSession, subscription: Subscription) -> RemnaWaveUser | None:
try:
@@ -300,14 +296,14 @@ class MonitoringService:
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск RemnaWave обновления подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск RemnaWave обновления подписки : обновлена вебхуком недавно',
+ subscription_id=subscription.id,
)
return None
user = await get_user_by_id(db, subscription.user_id)
if not user or not user.remnawave_uuid:
- logger.error(f'RemnaWave UUID не найден для пользователя {subscription.user_id}')
+ logger.error('RemnaWave UUID не найден для пользователя', user_id=subscription.user_id)
return None
# Обновляем subscription в сессии, чтобы избежать detached instance
@@ -319,8 +315,8 @@ class MonitoringService:
# Re-check guard after refresh (webhook could have committed between first check and refresh)
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск RemnaWave обновления подписки %s: обновлена вебхуком недавно (после refresh)',
- subscription.id,
+ 'Пропуск RemnaWave обновления подписки : обновлена вебхуком недавно (после refresh)',
+ subscription_id=subscription.id,
)
return None
@@ -331,12 +327,11 @@ class MonitoringService:
subscription.status = SubscriptionStatus.EXPIRED.value
await db.commit()
is_active = False
- logger.info(f"📝 Статус подписки {subscription.id} обновлен на 'expired'")
+ logger.info("📝 Статус подписки обновлен на 'expired'", subscription_id=subscription.id)
if not self.subscription_service.is_configured:
logger.warning(
- 'RemnaWave API не настроен. Пропускаем обновление пользователя %s',
- subscription.user_id,
+ 'RemnaWave API не настроен. Пропускаем обновление пользователя', user_id=subscription.user_id
)
return None
@@ -365,14 +360,18 @@ class MonitoringService:
await db.commit()
status_text = 'активным' if is_active else 'истёкшим'
- logger.info(f'✅ Обновлен RemnaWave пользователь {user.remnawave_uuid} со статусом {status_text}')
+ logger.info(
+ '✅ Обновлен RemnaWave пользователь со статусом',
+ remnawave_uuid=user.remnawave_uuid,
+ status_text=status_text,
+ )
return updated_user
except RemnaWaveAPIError as e:
- logger.error(f'Ошибка обновления RemnaWave пользователя: {e}')
+ logger.error('Ошибка обновления RemnaWave пользователя', error=e)
return None
except Exception as e:
- logger.error(f'Ошибка обновления RemnaWave пользователя: {e}')
+ logger.error('Ошибка обновления RemnaWave пользователя', error=e)
return None
async def _check_expiring_subscriptions(self, db: AsyncSession):
@@ -397,7 +396,11 @@ class MonitoringService:
await notification_sent(db, user.id, subscription.id, 'expiring', days)
or user_key in all_processed_users
):
- logger.debug(f'🔄 Пропускаем дублирование для пользователя {user_identifier} на {days} дней')
+ logger.debug(
+ '🔄 Пропускаем дублирование для пользователя на дней',
+ user_identifier=user_identifier,
+ days=days,
+ )
continue
should_send = True
@@ -407,7 +410,10 @@ class MonitoringService:
if any(s.user_id == user.id for s in other_subs):
should_send = False
logger.debug(
- f'🎯 Пропускаем уведомление на {days} дней для пользователя {user_identifier}, есть более срочное на {other_days} дней'
+ '🎯 Пропускаем уведомление на дней для пользователя есть более срочное на дней',
+ days=days,
+ user_identifier=user_identifier,
+ other_days=other_days,
)
break
@@ -426,7 +432,9 @@ class MonitoringService:
all_processed_users.add(user_key)
sent_count += 1
logger.info(
- f'✅ Email-пользователю {user.id} отправлено уведомление об истечении подписки через {days} дней'
+ '✅ Email-пользователю отправлено уведомление об истечении подписки через дней',
+ user_id=user.id,
+ days=days,
)
continue
@@ -437,10 +445,14 @@ class MonitoringService:
all_processed_users.add(user_key)
sent_count += 1
logger.info(
- f'✅ Пользователю {user.telegram_id} отправлено уведомление об истечении подписки через {days} дней'
+ '✅ Пользователю отправлено уведомление об истечении подписки через дней',
+ telegram_id=user.telegram_id,
+ days=days,
)
else:
- logger.warning(f'❌ Не удалось отправить уведомление пользователю {user.telegram_id}')
+ logger.warning(
+ '❌ Не удалось отправить уведомление пользователю', telegram_id=user.telegram_id
+ )
if sent_count > 0:
await self._log_monitoring_event(
@@ -451,7 +463,7 @@ class MonitoringService:
)
except Exception as e:
- logger.error(f'Ошибка проверки истекающих подписок: {e}')
+ logger.error('Ошибка проверки истекающих подписок', error=e)
async def _check_trial_expiring_soon(self, db: AsyncSession):
try:
@@ -489,7 +501,8 @@ class MonitoringService:
if success:
await record_notification(db, user.id, subscription.id, 'trial_2h')
logger.info(
- f'🎁 Пользователю {user.telegram_id} отправлено уведомление об окончании тестовой подписки через 2 часа'
+ '🎁 Пользователю отправлено уведомление об окончании тестовой подписки через 2 часа',
+ telegram_id=user.telegram_id,
)
if trial_expiring:
@@ -501,7 +514,7 @@ class MonitoringService:
)
except Exception as e:
- logger.error(f'Ошибка проверки истекающих тестовых подписок: {e}')
+ logger.error('Ошибка проверки истекающих тестовых подписок', error=e)
async def _check_trial_channel_subscriptions(self, db: AsyncSession):
from app.database.crud.subscription import is_recently_updated_by_webhook
@@ -569,41 +582,41 @@ class MonitoringService:
)
except TelegramForbiddenError as error:
logger.error(
- '❌ Не удалось проверить подписку пользователя %s на канал %s: бот заблокирован (%s)',
- user.telegram_id,
- channel_id,
- error,
+ '❌ Не удалось проверить подписку пользователя на канал : бот заблокирован',
+ telegram_id=user.telegram_id,
+ channel_id=channel_id,
+ error=error,
)
continue
except TelegramBadRequest as error:
# PARTICIPANT_ID_INVALID - пользователь никогда не был в канале, это нормально
logger.warning(
- '⚠️ Ошибка Telegram при проверке подписки пользователя %s: %s',
- user.telegram_id,
- error,
+ '⚠️ Ошибка Telegram при проверке подписки пользователя',
+ telegram_id=user.telegram_id,
+ error=error,
)
continue
except Exception as error:
logger.error(
- '❌ Неожиданная ошибка при проверке подписки пользователя %s: %s',
- user.telegram_id,
- error,
+ '❌ Неожиданная ошибка при проверке подписки пользователя',
+ telegram_id=user.telegram_id,
+ error=error,
)
continue
if subscription.status == SubscriptionStatus.ACTIVE.value and subscription.is_trial and not is_member:
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск деактивации trial подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск деактивации trial подписки : обновлена вебхуком недавно',
+ subscription_id=subscription.id,
)
continue
subscription = await deactivate_subscription(db, subscription)
disabled_count += 1
logger.info(
- '🚫 Триальная подписка пользователя %s (ID %s) отключена из-за отписки от канала',
- user.telegram_id,
- subscription.id,
+ '🚫 Триальная подписка пользователя (ID) отключена из-за отписки от канала',
+ telegram_id=user.telegram_id,
+ subscription_id=subscription.id,
)
if user.remnawave_uuid:
@@ -611,9 +624,9 @@ class MonitoringService:
await self.subscription_service.disable_remnawave_user(user.remnawave_uuid)
except Exception as api_error:
logger.error(
- '❌ Не удалось отключить пользователя RemnaWave %s: %s',
- user.remnawave_uuid,
- api_error,
+ '❌ Не удалось отключить пользователя RemnaWave',
+ remnawave_uuid=user.remnawave_uuid,
+ api_error=api_error,
)
if notifications_allowed:
@@ -634,8 +647,8 @@ class MonitoringService:
elif subscription.status == SubscriptionStatus.DISABLED.value and subscription.is_trial and is_member:
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск реактивации trial подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск реактивации trial подписки : обновлена вебхуком недавно',
+ subscription_id=subscription.id,
)
continue
subscription.status = SubscriptionStatus.ACTIVE.value
@@ -645,9 +658,9 @@ class MonitoringService:
restored_count += 1
logger.info(
- '✅ Триальная подписка пользователя %s (ID %s) восстановлена после повторной подписки на канал',
- user.telegram_id,
- subscription.id,
+ '✅ Триальная подписка пользователя (ID) восстановлена после повторной подписки на канал',
+ telegram_id=user.telegram_id,
+ subscription_id=subscription.id,
)
try:
@@ -657,9 +670,9 @@ class MonitoringService:
await self.subscription_service.create_remnawave_user(db, subscription)
except Exception as api_error:
logger.error(
- '❌ Не удалось обновить RemnaWave пользователя %s: %s',
- user.telegram_id,
- api_error,
+ '❌ Не удалось обновить RemnaWave пользователя',
+ telegram_id=user.telegram_id,
+ api_error=api_error,
)
await clear_notification_by_type(
@@ -684,7 +697,7 @@ class MonitoringService:
)
except Exception as error:
- logger.error(f'Ошибка проверки подписки на канал для триальных пользователей: {error}')
+ logger.error('Ошибка проверки подписки на канал для триальных пользователей', error=error)
async def _check_expired_subscription_followups(self, db: AsyncSession):
if not NotificationSettingsService.are_notifications_globally_enabled():
@@ -812,7 +825,7 @@ class MonitoringService:
)
except Exception as e:
- logger.error(f'Ошибка проверки напоминаний об истекшей подписке: {e}')
+ logger.error('Ошибка проверки напоминаний об истекшей подписке', error=e)
async def _get_expiring_paid_subscriptions(self, db: AsyncSession, days_before: int) -> list[Subscription]:
current_time = datetime.utcnow()
@@ -834,9 +847,9 @@ class MonitoringService:
)
)
- logger.debug(f'🔍 Поиск платных подписок, истекающих в ближайшие {days_before} дней')
- logger.debug(f'📅 Текущее время: {current_time}')
- logger.debug(f'📅 Пороговая дата: {threshold_date}')
+ logger.debug('🔍 Поиск платных подписок, истекающих в ближайшие дней', days_before=days_before)
+ logger.debug('📅 Текущее время', current_time=current_time)
+ logger.debug('📅 Пороговая дата', threshold_date=threshold_date)
all_subscriptions = result.scalars().all()
@@ -847,9 +860,9 @@ class MonitoringService:
excluded_count = len(all_subscriptions) - len(subscriptions)
if excluded_count > 0:
- logger.debug(f'🔄 Исключено {excluded_count} суточных подписок из уведомлений')
+ logger.debug('🔄 Исключено суточных подписок из уведомлений', excluded_count=excluded_count)
- logger.info(f'📊 Найдено {len(subscriptions)} платных подписок для уведомлений')
+ logger.info('📊 Найдено платных подписок для уведомлений', subscriptions_count=len(subscriptions))
return subscriptions
@@ -887,9 +900,7 @@ class MonitoringService:
offer = await get_latest_claimed_offer_for_user(db, user.id, source)
except Exception as lookup_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to resolve latest claimed promo offer for user %s: %s',
- user.id,
- lookup_error,
+ 'Failed to resolve latest claimed promo offer for user', user_id=user.id, lookup_error=lookup_error
)
offer = None
@@ -919,17 +930,12 @@ class MonitoringService:
details={'reason': 'autopay_consumed'},
)
except Exception as log_error: # pragma: no cover - defensive logging
- logger.warning(
- 'Failed to record promo offer autopay log for user %s: %s',
- user.id,
- log_error,
- )
+ logger.warning('Failed to record promo offer autopay log for user', user_id=user.id, log_error=log_error)
try:
await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to rollback session after promo offer autopay log failure: %s',
- rollback_error,
+ 'Failed to rollback session after promo offer autopay log failure', rollback_error=rollback_error
)
async def _process_autopayments(self, db: AsyncSession):
@@ -961,9 +967,7 @@ class MonitoringService:
# (DailySubscriptionService), глобальный autopay на них не распространяется
if sub.tariff and getattr(sub.tariff, 'is_daily', False):
logger.debug(
- 'Пропускаем суточную подписку %s (тариф %s) в глобальном autopay',
- sub.id,
- sub.tariff.name,
+ 'Пропускаем суточную подписку (тариф) в глобальном autopay', sub_id=sub.id, name=sub.tariff.name
)
continue
@@ -979,8 +983,7 @@ class MonitoringService:
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск автоплатежа подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск автоплатежа подписки : обновлена вебхуком недавно', subscription_id=subscription.id
)
continue
@@ -1035,10 +1038,10 @@ class MonitoringService:
processed_count += 1
self._notified_users.add(autopay_key)
logger.info(
- '💳 Автопродление подписки пользователя %s успешно (списано %s, скидка %s%%)',
- user_identifier,
- charge_amount,
- promo_discount_percent,
+ '💳 Автопродление подписки пользователя успешно (списано , скидка %)',
+ user_identifier=user_identifier,
+ charge_amount=charge_amount,
+ promo_discount_percent=promo_discount_percent,
)
else:
failed_count += 1
@@ -1049,7 +1052,9 @@ class MonitoringService:
user=user,
reason='Ошибка списания средств',
)
- logger.warning(f'💳 Ошибка списания средств для автопродления пользователя {user_identifier}')
+ logger.warning(
+ '💳 Ошибка списания средств для автопродления пользователя', user_identifier=user_identifier
+ )
else:
failed_count += 1
@@ -1062,15 +1067,15 @@ class MonitoringService:
if await cache.exists(cooldown_key):
should_notify = False
logger.debug(
- '💳 Пропуск уведомления о недостаточном балансе для пользователя %s — кулдаун активен',
- user_identifier,
+ '💳 Пропуск уведомления о недостаточном балансе для пользователя — кулдаун активен',
+ user_identifier=user_identifier,
)
except Exception as redis_err:
# Fallback: если Redis недоступен — отправляем уведомление
logger.warning(
- '⚠️ Ошибка проверки кулдауна в Redis для пользователя %s: %s. Отправляем уведомление.',
- user_identifier,
- redis_err,
+ '⚠️ Ошибка проверки кулдауна в Redis для пользователя : . Отправляем уведомление.',
+ user_identifier=user_identifier,
+ redis_err=redis_err,
)
if should_notify:
@@ -1091,12 +1096,14 @@ class MonitoringService:
)
except Exception as redis_err:
logger.warning(
- '⚠️ Не удалось установить кулдаун в Redis для пользователя %s: %s',
- user_identifier,
- redis_err,
+ '⚠️ Не удалось установить кулдаун в Redis для пользователя',
+ user_identifier=user_identifier,
+ redis_err=redis_err,
)
- logger.warning(f'💳 Недостаточно средств для автопродления у пользователя {user_identifier}')
+ logger.warning(
+ '💳 Недостаточно средств для автопродления у пользователя', user_identifier=user_identifier
+ )
if processed_count > 0 or failed_count > 0:
await self._log_monitoring_event(
@@ -1107,7 +1114,7 @@ class MonitoringService:
)
except Exception as e:
- logger.error(f'Ошибка обработки автоплатежей: {e}')
+ logger.error('Ошибка обработки автоплатежей', error=e)
async def _send_subscription_expired_notification(self, user: User) -> bool:
try:
@@ -1140,16 +1147,14 @@ class MonitoringService:
if self._handle_unreachable_user(user, exc, 'уведомление об истечении подписки'):
return True
logger.error(
- 'Ошибка Telegram API при отправке уведомления об истечении подписки пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке уведомления об истечении подписки пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
return False
except Exception as e:
logger.error(
- 'Ошибка отправки уведомления об истечении подписки пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Ошибка отправки уведомления об истечении подписки пользователю', telegram_id=user.telegram_id, e=e
)
return False
@@ -1209,23 +1214,19 @@ class MonitoringService:
if self._handle_unreachable_user(user, exc, 'уведомление об истекающей подписке'):
return True
logger.error(
- 'Ошибка Telegram API при отправке уведомления об истечении подписки пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке уведомления об истечении подписки пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
return False
except TelegramNetworkError as e:
logger.warning(
- 'Таймаут отправки уведомления об истечении подписки пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Таймаут отправки уведомления об истечении подписки пользователю', telegram_id=user.telegram_id, e=e
)
return False
except Exception as e:
logger.error(
- 'Ошибка отправки уведомления об истечении подписки пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Ошибка отправки уведомления об истечении подписки пользователю', telegram_id=user.telegram_id, e=e
)
return False
@@ -1265,23 +1266,23 @@ class MonitoringService:
if self._handle_unreachable_user(user, exc, 'уведомление о завершении тестовой подписки'):
return True
logger.error(
- 'Ошибка Telegram API при отправке уведомления о завершении тестовой подписки пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке уведомления о завершении тестовой подписки пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
return False
except TelegramNetworkError as e:
logger.warning(
- 'Таймаут отправки уведомления об окончании тестовой подписки пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Таймаут отправки уведомления об окончании тестовой подписки пользователю',
+ telegram_id=user.telegram_id,
+ e=e,
)
return False
except Exception as e:
logger.error(
- 'Ошибка отправки уведомления об окончании тестовой подписки пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Ошибка отправки уведомления об окончании тестовой подписки пользователю',
+ telegram_id=user.telegram_id,
+ e=e,
)
return False
@@ -1335,23 +1336,23 @@ class MonitoringService:
if self._handle_unreachable_user(user, exc, 'уведомление об отписке от канала'):
return True
logger.error(
- 'Ошибка Telegram API при отправке уведомления об отписке от канала пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке уведомления об отписке от канала пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
return False
except TelegramNetworkError as error:
logger.warning(
- 'Таймаут отправки уведомления об отписке от канала пользователю %s: %s',
- user.telegram_id,
- error,
+ 'Таймаут отправки уведомления об отписке от канала пользователю',
+ telegram_id=user.telegram_id,
+ error=error,
)
return False
except Exception as error:
logger.error(
- 'Ошибка отправки уведомления об отписке от канала пользователю %s: %s',
- user.telegram_id,
- error,
+ 'Ошибка отправки уведомления об отписке от канала пользователю',
+ telegram_id=user.telegram_id,
+ error=error,
)
return False
@@ -1406,23 +1407,19 @@ class MonitoringService:
if self._handle_unreachable_user(user, exc, 'напоминание об истекшей подписке'):
return True
logger.error(
- 'Ошибка Telegram API при отправке напоминания об истекшей подписке пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке напоминания об истекшей подписке пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
return False
except TelegramNetworkError as e:
logger.warning(
- 'Таймаут отправки напоминания об истекшей подписке пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Таймаут отправки напоминания об истекшей подписке пользователю', telegram_id=user.telegram_id, e=e
)
return False
except Exception as e:
logger.error(
- 'Ошибка отправки напоминания об истекшей подписке пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Ошибка отправки напоминания об истекшей подписке пользователю', telegram_id=user.telegram_id, e=e
)
return False
@@ -1505,24 +1502,16 @@ class MonitoringService:
if self._handle_unreachable_user(user, exc, 'скидочное уведомление'):
return True
logger.error(
- 'Ошибка Telegram API при отправке скидочного уведомления пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке скидочного уведомления пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
return False
except TelegramNetworkError as e:
- logger.warning(
- 'Таймаут отправки скидочного уведомления пользователю %s: %s',
- user.telegram_id,
- e,
- )
+ logger.warning('Таймаут отправки скидочного уведомления пользователю', telegram_id=user.telegram_id, e=e)
return False
except Exception as e:
- logger.error(
- 'Ошибка отправки скидочного уведомления пользователю %s: %s',
- user.telegram_id,
- e,
- )
+ logger.error('Ошибка отправки скидочного уведомления пользователю', telegram_id=user.telegram_id, e=e)
return False
async def _send_autopay_success_notification(self, user: User, amount: int, days: int):
@@ -1537,22 +1526,16 @@ class MonitoringService:
except (TelegramForbiddenError, TelegramBadRequest) as exc:
if not self._handle_unreachable_user(user, exc, 'уведомление об успешном автоплатеже'):
logger.error(
- 'Ошибка Telegram API при отправке уведомления об автоплатеже пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке уведомления об автоплатеже пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
except TelegramNetworkError as e:
logger.warning(
- 'Таймаут отправки уведомления об автоплатеже пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Таймаут отправки уведомления об автоплатеже пользователю', telegram_id=user.telegram_id, e=e
)
except Exception as e:
- logger.error(
- 'Ошибка отправки уведомления об автоплатеже пользователю %s: %s',
- user.telegram_id,
- e,
- )
+ logger.error('Ошибка отправки уведомления об автоплатеже пользователю', telegram_id=user.telegram_id, e=e)
async def _send_autopay_failed_notification(self, user: User, balance: int, required: int):
try:
@@ -1580,21 +1563,17 @@ class MonitoringService:
except (TelegramForbiddenError, TelegramBadRequest) as exc:
if not self._handle_unreachable_user(user, exc, 'уведомление о неудачном автоплатеже'):
logger.error(
- 'Ошибка Telegram API при отправке уведомления о неудачном автоплатеже пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка Telegram API при отправке уведомления о неудачном автоплатеже пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
except TelegramNetworkError as e:
logger.warning(
- 'Таймаут отправки уведомления о неудачном автоплатеже пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Таймаут отправки уведомления о неудачном автоплатеже пользователю', telegram_id=user.telegram_id, e=e
)
except Exception as e:
logger.error(
- 'Ошибка отправки уведомления о неудачном автоплатеже пользователю %s: %s',
- user.telegram_id,
- e,
+ 'Ошибка отправки уведомления о неудачном автоплатеже пользователю', telegram_id=user.telegram_id, e=e
)
async def _cleanup_inactive_users(self, db: AsyncSession):
@@ -1619,10 +1598,10 @@ class MonitoringService:
f'Удалено {deleted_count} неактивных пользователей',
{'deleted_count': deleted_count},
)
- logger.info(f'🗑️ Удалено {deleted_count} неактивных пользователей')
+ logger.info('🗑️ Удалено неактивных пользователей', deleted_count=deleted_count)
except Exception as e:
- logger.error(f'Ошибка очистки неактивных пользователей: {e}')
+ logger.error('Ошибка очистки неактивных пользователей', error=e)
async def _sync_with_remnawave(self, db: AsyncSession):
try:
@@ -1642,7 +1621,7 @@ class MonitoringService:
)
except Exception as e:
- logger.error(f'Ошибка синхронизации с RemnaWave: {e}')
+ logger.error('Ошибка синхронизации с RemnaWave', error=e)
await self._log_monitoring_event(
db,
'remnawave_sync_error',
@@ -1732,7 +1711,9 @@ class MonitoringService:
# commit after each to persist timestamp and avoid duplicate reminders on crash
await db.commit()
except Exception as notify_error:
- logger.error(f'Ошибка отправки SLA-уведомления по тикету {ticket.id}: {notify_error}')
+ logger.error(
+ 'Ошибка отправки SLA-уведомления по тикету', ticket_id=ticket.id, notify_error=notify_error
+ )
if reminders_sent > 0:
await self._log_monitoring_event(
@@ -1742,7 +1723,7 @@ class MonitoringService:
{'count': reminders_sent},
)
except Exception as e:
- logger.error(f'Ошибка проверки SLA тикетов: {e}')
+ logger.error('Ошибка проверки SLA тикетов', error=e)
async def _sla_loop(self):
try:
@@ -1756,12 +1737,12 @@ class MonitoringService:
await self._check_ticket_sla(db)
await db.commit()
except Exception as e:
- logger.error(f'Ошибка в SLA-проверке: {e}')
+ logger.error('Ошибка в SLA-проверке', error=e)
await db.rollback()
except asyncio.CancelledError:
break
except Exception as e:
- logger.error(f'Ошибка в SLA-цикле: {e}')
+ logger.error('Ошибка в SLA-цикле', error=e)
await asyncio.sleep(interval_seconds)
async def _log_monitoring_event(
@@ -1774,7 +1755,7 @@ class MonitoringService:
await db.commit()
except Exception as e:
- logger.error(f'Ошибка логирования события мониторинга: {e}')
+ logger.error('Ошибка логирования события мониторинга', error=e)
async def get_monitoring_status(self, db: AsyncSession) -> dict[str, Any]:
try:
@@ -1814,7 +1795,7 @@ class MonitoringService:
}
except Exception as e:
- logger.error(f'Ошибка получения статуса мониторинга: {e}')
+ logger.error('Ошибка получения статуса мониторинга', error=e)
return {
'is_running': self.is_running,
'last_update': datetime.utcnow(),
@@ -1832,8 +1813,7 @@ class MonitoringService:
for subscription in expired_subscriptions:
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск force-check подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск force-check подписки : обновлена вебхуком недавно', subscription_id=subscription.id
)
continue
await deactivate_subscription(db, subscription)
@@ -1860,7 +1840,7 @@ class MonitoringService:
return {'expired': expired_count, 'expiring': expiring_count, 'autopay_ready': autopay_processed}
except Exception as e:
- logger.error(f'Ошибка принудительной проверки подписок: {e}')
+ logger.error('Ошибка принудительной проверки подписок', error=e)
return {'expired': 0, 'expiring': 0, 'autopay_ready': 0}
async def get_monitoring_logs(
@@ -1896,7 +1876,7 @@ class MonitoringService:
]
except Exception as e:
- logger.error(f'Ошибка получения логов мониторинга: {e}')
+ logger.error('Ошибка получения логов мониторинга', error=e)
return []
async def get_monitoring_logs_count(self, db: AsyncSession, event_type: str | None = None) -> int:
@@ -1914,7 +1894,7 @@ class MonitoringService:
return count or 0
except Exception as e:
- logger.error(f'Ошибка получения количества логов: {e}')
+ logger.error('Ошибка получения количества логов', error=e)
return 0
async def get_monitoring_event_types(self, db: AsyncSession) -> list[str]:
@@ -1931,7 +1911,7 @@ class MonitoringService:
return [row[0] for row in result.fetchall() if row[0]]
except Exception as e:
- logger.error(f'Ошибка получения списка типов событий мониторинга: {e}')
+ logger.error('Ошибка получения списка типов событий мониторинга', error=e)
return []
async def cleanup_old_logs(self, db: AsyncSession, days: int = 30) -> int:
@@ -1948,14 +1928,14 @@ class MonitoringService:
await db.commit()
if days == 0:
- logger.info(f'🗑️ Удалены все логи мониторинга ({deleted_count} записей)')
+ logger.info('🗑️ Удалены все логи мониторинга ( записей)', deleted_count=deleted_count)
else:
- logger.info(f'🗑️ Удалено {deleted_count} старых записей логов (старше {days} дней)')
+ logger.info('🗑️ Удалено старых записей логов (старше дней)', deleted_count=deleted_count, days=days)
return deleted_count
except Exception as e:
- logger.error(f'Ошибка очистки логов: {e}')
+ logger.error('Ошибка очистки логов', error=e)
await db.rollback()
return 0
diff --git a/app/services/mulenpay_service.py b/app/services/mulenpay_service.py
index 87a66cbe..2957789c 100644
--- a/app/services/mulenpay_service.py
+++ b/app/services/mulenpay_service.py
@@ -1,15 +1,15 @@
import asyncio
import hashlib
import json
-import logging
from typing import Any
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class MulenPayService:
@@ -65,10 +65,7 @@ class MulenPayService:
if response.status >= 400:
logger.error(
- 'MulenPay API error %s %s: %s',
- response.status,
- endpoint,
- raw_text,
+ 'MulenPay API error', response_status=response.status, endpoint=endpoint, raw_text=raw_text
)
if response.status in self._retryable_statuses and attempt < self._max_retries:
await self._sleep_with_backoff(attempt)
@@ -78,37 +75,35 @@ class MulenPayService:
if data is None:
if raw_text:
logger.warning(
- 'MulenPay returned unexpected payload for %s: %s',
- endpoint,
- raw_text,
+ 'MulenPay returned unexpected payload for', endpoint=endpoint, raw_text=raw_text
)
return None
return data
except asyncio.CancelledError:
- logger.debug('MulenPay request cancelled: %s %s', method, endpoint)
+ logger.debug('MulenPay request cancelled', method=method, endpoint=endpoint)
raise
except TimeoutError as error:
last_error = error
logger.warning(
- 'MulenPay request timeout (%s %s) attempt %s/%s',
- method,
- endpoint,
- attempt,
- self._max_retries,
+ 'MulenPay request timeout attempt /',
+ method=method,
+ endpoint=endpoint,
+ attempt=attempt,
+ max_retries=self._max_retries,
)
except aiohttp.ClientError as error:
last_error = error
logger.warning(
- 'MulenPay client error (%s %s) attempt %s/%s: %s',
- method,
- endpoint,
- attempt,
- self._max_retries,
- error,
+ 'MulenPay client error attempt /',
+ method=method,
+ endpoint=endpoint,
+ attempt=attempt,
+ max_retries=self._max_retries,
+ error=error,
)
except Exception as error: # pragma: no cover - safety
- logger.error('Unexpected MulenPay error: %s', error, exc_info=True)
+ logger.error('Unexpected MulenPay error', error=error, exc_info=True)
return None
if attempt < self._max_retries:
@@ -116,18 +111,18 @@ class MulenPayService:
if isinstance(last_error, asyncio.TimeoutError):
logger.error(
- 'MulenPay request timed out after %s attempts: %s %s',
- self._max_retries,
- method,
- endpoint,
+ 'MulenPay request timed out after attempts',
+ max_retries=self._max_retries,
+ method=method,
+ endpoint=endpoint,
)
elif last_error is not None:
logger.error(
- 'MulenPay request failed after %s attempts (%s %s): %s',
- self._max_retries,
- method,
- endpoint,
- last_error,
+ 'MulenPay request failed after attempts',
+ max_retries=self._max_retries,
+ method=method,
+ endpoint=endpoint,
+ last_error=last_error,
)
return None
@@ -145,11 +140,7 @@ class MulenPayService:
try:
return json.loads(raw_text), raw_text
except json.JSONDecodeError as error:
- logger.error(
- 'Failed to decode MulenPay JSON response %s: %s',
- response.url,
- error,
- )
+ logger.error('Failed to decode MulenPay JSON response', url=response.url, error=error)
return None, raw_text
return None, raw_text
@@ -200,7 +191,7 @@ class MulenPayService:
response = await self._request('POST', '/v2/payments', json_data=payload)
if not response or not response.get('success'):
- logger.error('Failed to create MulenPay payment: %s', response)
+ logger.error('Failed to create MulenPay payment', response=response)
return None
return response
diff --git a/app/services/nalogo_queue_service.py b/app/services/nalogo_queue_service.py
index 9ba07563..25d73917 100644
--- a/app/services/nalogo_queue_service.py
+++ b/app/services/nalogo_queue_service.py
@@ -5,9 +5,9 @@
"""
import asyncio
-import logging
from datetime import datetime, timedelta
+import structlog
from aiogram import Bot
from dateutil.parser import isoparse
@@ -16,7 +16,7 @@ from app.services.nalogo_service import NaloGoService
from app.utils.cache import cache
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class NalogoQueueService:
@@ -71,8 +71,9 @@ class NalogoQueueService:
self._running = True
self._task = asyncio.create_task(self._process_queue_loop())
logger.info(
- f'Сервис очереди чеков NaloGO запущен '
- f'(интервал: {self._check_interval}с, задержка между чеками: {self._receipt_delay}с)'
+ 'Сервис очереди чеков NaloGO запущен (интервал: с, задержка между чеками: с)',
+ _check_interval=self._check_interval,
+ _receipt_delay=self._receipt_delay,
)
async def stop(self) -> None:
@@ -116,7 +117,7 @@ class NalogoQueueService:
self._last_notification_time = datetime.now()
logger.info('Отправлено уведомление о чеках NaloGO')
except Exception as error:
- logger.error(f'Ошибка отправки уведомления о чеках: {error}')
+ logger.error('Ошибка отправки уведомления о чеках', error=error)
async def _process_queue_loop(self) -> None:
"""Основной цикл обработки очереди."""
@@ -124,7 +125,7 @@ class NalogoQueueService:
try:
await self._process_pending_receipts()
except Exception as error:
- logger.error(f'Ошибка в цикле обработки очереди чеков: {error}')
+ logger.error('Ошибка в цикле обработки очереди чеков', error=error)
await asyncio.sleep(self._check_interval)
@@ -137,7 +138,7 @@ class NalogoQueueService:
if queue_length == 0:
return
- logger.info(f'Начинаем обработку очереди чеков: {queue_length} шт.')
+ logger.info('Начинаем обработку очереди чеков: шт.', queue_length=queue_length)
self._had_pending_receipts = True
processed = 0
@@ -157,7 +158,7 @@ class NalogoQueueService:
# Логируем количество попыток (чек никогда не удаляется из очереди)
if attempts >= 10:
- logger.warning(f'Чек {payment_id} уже {attempts} попыток, продолжаем пытаться...')
+ logger.warning('Чек уже попыток, продолжаем пытаться...', payment_id=payment_id, attempts=attempts)
# Пытаемся отправить чек
try:
@@ -172,7 +173,9 @@ class NalogoQueueService:
try:
operation_time = isoparse(created_at_str)
except (ValueError, TypeError) as parse_error:
- logger.warning(f"Не удалось распарсить created_at '{created_at_str}': {parse_error}")
+ logger.warning(
+ 'Не удалось распарсить created_at', created_at_str=created_at_str, parse_error=parse_error
+ )
# Формируем описание заново из настроек (если есть данные)
if amount_kopeks is not None:
@@ -205,8 +208,10 @@ class NalogoQueueService:
await cache.delete(queued_key)
logger.info(
- f'Чек из очереди успешно создан: {receipt_uuid} '
- f'(payment_id={payment_id}, попытка {attempts + 1})'
+ 'Чек из очереди успешно создан: (payment_id=, попытка )',
+ receipt_uuid=receipt_uuid,
+ payment_id=payment_id,
+ attempts=attempts + 1,
)
else:
# Вернуть в очередь с увеличенным счетчиком попыток
@@ -214,8 +219,10 @@ class NalogoQueueService:
failed += 1
service_unavailable = True
logger.warning(
- f'Не удалось создать чек из очереди (payment_id={payment_id}), '
- f'возвращен в очередь (попытка {attempts + 1}/{self._max_attempts})'
+ 'Не удалось создать чек из очереди (payment_id=), возвращен в очередь (попытка /)',
+ payment_id=payment_id,
+ attempts=attempts + 1,
+ _max_attempts=self._max_attempts,
)
# Если сервис недоступен, прекращаем попытки до следующего цикла
break
@@ -223,7 +230,7 @@ class NalogoQueueService:
except Exception as error:
await self._nalogo_service.requeue_receipt(receipt_data)
failed += 1
- logger.error(f'Ошибка при создании чека из очереди (payment_id={payment_id}): {error}')
+ logger.error('Ошибка при создании чека из очереди (payment_id=)', payment_id=payment_id, error=error)
# Прекращаем попытки при ошибке
break
@@ -231,7 +238,12 @@ class NalogoQueueService:
await asyncio.sleep(self._receipt_delay)
if processed > 0 or failed > 0 or skipped > 0:
- logger.info(f'Обработка очереди завершена: успешно={processed}, неудачно={failed}, пропущено={skipped}')
+ logger.info(
+ 'Обработка очереди завершена: успешно=, неудачно=, пропущено',
+ processed=processed,
+ failed=failed,
+ skipped=skipped,
+ )
# Проверяем остаток в очереди
remaining = await self._nalogo_service.get_queue_length()
diff --git a/app/services/nalogo_service.py b/app/services/nalogo_service.py
index 443e0348..0cb8be31 100644
--- a/app/services/nalogo_service.py
+++ b/app/services/nalogo_service.py
@@ -1,8 +1,9 @@
-import logging
from datetime import date, datetime, timedelta
from decimal import Decimal
from typing import Any
+import structlog
+
from app.config import settings
# Используем локальную исправленную версию библиотеки
@@ -11,7 +12,7 @@ from app.lib.nalogo.dto.income import IncomeClient, IncomeType
from app.utils.cache import cache
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
NALOGO_QUEUE_KEY = 'nalogo:receipt_queue'
NALOGO_PENDING_VERIFICATION_KEY = 'nalogo:pending_verification'
@@ -49,13 +50,9 @@ class NaloGoService:
self.inn = inn
self.password = password
self.configured = True
- logger.info(f'NaloGO клиент инициализирован для ИНН: {inn[:5]}...')
+ logger.info('NaloGO клиент инициализирован для ИНН: ...', inn=inn[:5])
except Exception as error:
- logger.error(
- 'Ошибка инициализации NaloGO клиента: %s',
- error,
- exc_info=True,
- )
+ logger.error('Ошибка инициализации NaloGO клиента', error=error, exc_info=True)
self.configured = False
@staticmethod
@@ -97,7 +94,11 @@ class NaloGoService:
created_key = f'nalogo:created:{payment_id}'
already_created = await cache.get(created_key)
if already_created:
- logger.info(f'Чек для payment_id={payment_id} уже создан ({already_created}), не добавляем в очередь')
+ logger.info(
+ 'Чек для payment_id= уже создан не добавляем в очередь',
+ payment_id=payment_id,
+ already_created=already_created,
+ )
return False
# Атомарная проверка и установка флага "в очереди" (защита от race condition)
@@ -105,7 +106,7 @@ class NaloGoService:
lock_acquired = await cache.setnx(queued_key, 'queued', expire=7 * 24 * 3600)
if not lock_acquired:
# Ключ уже существует — чек уже в очереди
- logger.info(f'Чек для payment_id={payment_id} уже в очереди, пропускаем дубликат')
+ logger.info('Чек для payment_id= уже в очереди, пропускаем дубликат', payment_id=payment_id)
return False
receipt_data = {
@@ -122,7 +123,12 @@ class NaloGoService:
success = await cache.lpush(NALOGO_QUEUE_KEY, receipt_data)
if success:
queue_len = await cache.llen(NALOGO_QUEUE_KEY)
- logger.info(f'Чек добавлен в очередь (payment_id={payment_id}, сумма={amount}₽, в очереди: {queue_len})')
+ logger.info(
+ 'Чек добавлен в очередь (payment_id=, сумма=₽, в очереди: )',
+ payment_id=payment_id,
+ amount=amount,
+ queue_len=queue_len,
+ )
# Если не удалось добавить в очередь — удаляем флаг
elif payment_id:
queued_key = f'nalogo:queued:{payment_id}'
@@ -161,8 +167,10 @@ class NaloGoService:
if success:
count = await cache.llen(NALOGO_PENDING_VERIFICATION_KEY)
logger.warning(
- f'Чек сохранён для ручной проверки (payment_id={payment_id}, '
- f'сумма={amount}₽, всего ожидают проверки: {count})'
+ 'Чек сохранён для ручной проверки (payment_id=, сумма=₽, всего ожидают проверки: )',
+ payment_id=payment_id,
+ amount=amount,
+ count=count,
)
return success
@@ -201,7 +209,7 @@ class NaloGoService:
# Сохраняем что чек создан
created_key = f'nalogo:created:{payment_id}'
await cache.set(created_key, receipt_uuid, expire=30 * 24 * 3600)
- logger.info(f'Чек {payment_id} помечен как созданный: {receipt_uuid}')
+ logger.info('Чек помечен как созданный', payment_id=payment_id, receipt_uuid=receipt_uuid)
else:
updated_receipts.append(receipt)
@@ -210,7 +218,7 @@ class NaloGoService:
await cache.delete(NALOGO_PENDING_VERIFICATION_KEY)
for r in reversed(updated_receipts): # reversed чтобы сохранить порядок
await cache.lpush(NALOGO_PENDING_VERIFICATION_KEY, r)
- logger.info(f'Чек {payment_id} удалён из очереди проверки')
+ logger.info('Чек удалён из очереди проверки', payment_id=payment_id)
return removed_receipt
@@ -231,7 +239,7 @@ class NaloGoService:
break
if not target_receipt:
- logger.warning(f'Чек {payment_id} не найден в очереди проверки')
+ logger.warning('Чек не найден в очереди проверки', payment_id=payment_id)
return None
# Пытаемся создать чек
@@ -249,7 +257,7 @@ class NaloGoService:
if receipt_uuid:
# Удаляем из очереди проверки
await self.mark_pending_as_verified(payment_id, receipt_uuid, was_created=True)
- logger.info(f'Чек {payment_id} успешно создан после ручной проверки: {receipt_uuid}')
+ logger.info('Чек успешно создан после ручной проверки', payment_id=payment_id, receipt_uuid=receipt_uuid)
return receipt_uuid
@@ -258,7 +266,7 @@ class NaloGoService:
count = await self.get_pending_verification_count()
if count > 0:
await cache.delete(NALOGO_PENDING_VERIFICATION_KEY)
- logger.info(f'Очередь проверки очищена: удалено {count} чеков')
+ logger.info('Очередь проверки очищена: удалено чеков', count=count)
return count
async def authenticate(self) -> bool:
@@ -273,9 +281,9 @@ class NaloGoService:
return True
except Exception as error:
if self._is_service_unavailable(error):
- logger.warning('NaloGO временно недоступен (техработы): %s', str(error)[:200])
+ logger.warning('NaloGO временно недоступен (техработы)', error=str(error)[:200])
else:
- logger.error('Ошибка аутентификации в NaloGO: %s', error, exc_info=True)
+ logger.error('Ошибка аутентификации в NaloGO', error=error, exc_info=True)
return False
async def create_receipt(
@@ -316,7 +324,9 @@ class NaloGoService:
already_created = await cache.get(created_key)
if already_created:
logger.info(
- f'Чек для payment_id={payment_id} уже был создан ({already_created}), пропускаем повторное создание'
+ 'Чек для payment_id= уже был создан пропускаем повторное создание',
+ payment_id=payment_id,
+ already_created=already_created,
)
return already_created # Возвращаем ранее созданный uuid
@@ -336,14 +346,16 @@ class NaloGoService:
# Ошибка аутентификации — чек не создавался, безопасно в очередь
if self._is_service_unavailable(auth_error):
logger.warning(
- f'NaloGO недоступен при аутентификации, чек в очередь (payment_id={payment_id}, сумма={amount}₽)'
+ 'NaloGO недоступен при аутентификации, чек в очередь (payment_id=, сумма=₽)',
+ payment_id=payment_id,
+ amount=amount,
)
if queue_on_failure:
await self._queue_receipt(
name, amount, quantity, client_info, payment_id, telegram_user_id, amount_kopeks
)
else:
- logger.error('Ошибка аутентификации NaloGO: %s', auth_error, exc_info=True)
+ logger.error('Ошибка аутентификации NaloGO', auth_error=auth_error, exc_info=True)
return None
# ЭТАП 2: Создание чека
@@ -373,7 +385,7 @@ class NaloGoService:
receipt_uuid = result.get('approvedReceiptUuid')
if receipt_uuid:
- logger.info(f'Чек создан успешно: {receipt_uuid} на сумму {amount}₽')
+ logger.info('Чек создан успешно: на сумму ₽', receipt_uuid=receipt_uuid, amount=amount)
# Сохраняем в Redis чтобы предотвратить дубли (TTL 30 дней)
if payment_id:
@@ -381,7 +393,7 @@ class NaloGoService:
await cache.set(created_key, receipt_uuid, expire=30 * 24 * 3600)
return receipt_uuid
- logger.error(f'Ошибка создания чека: {result}')
+ logger.error('Ошибка создания чека', result=result)
return None
except Exception as error:
@@ -390,9 +402,9 @@ class NaloGoService:
if self._is_service_unavailable(error):
error_msg = str(error)[:200]
logger.error(
- f'⚠️ ТАЙМАУТ после успешной аутентификации! Чек МОГ быть создан! '
- f'(payment_id={payment_id}, сумма={amount}₽). '
- f'Сохраняем в очередь проверки. Проверьте lknpd.nalog.ru'
+ '⚠️ ТАЙМАУТ после успешной аутентификации! Чек МОГ быть создан! (payment_id=, сумма=₽). Сохраняем в очередь проверки. Проверьте lknpd.nalog.ru',
+ payment_id=payment_id,
+ amount=amount,
)
# Сохраняем в очередь для ручной проверки
await self._save_pending_verification(
@@ -406,7 +418,7 @@ class NaloGoService:
error_message=error_msg,
)
else:
- logger.error('Ошибка создания чека в NaloGO: %s', error, exc_info=True)
+ logger.error('Ошибка создания чека в NaloGO', error=error, exc_info=True)
return None
async def get_queue_length(self) -> int:
@@ -488,19 +500,21 @@ class NaloGoService:
receipt_uuid = income.get('approvedReceiptUuid', income.get('receiptUuid'))
if receipt_uuid:
logger.info(
- f'Найден дубликат чека: {receipt_uuid} '
- f'(сумма={income_amount}₽, время={operation_time}, '
- f'разница={time_diff:.0f}с)'
+ 'Найден дубликат чека: (сумма=₽, время=, разница=с)',
+ receipt_uuid=receipt_uuid,
+ income_amount=income_amount,
+ operation_time=operation_time,
+ time_diff=round(time_diff, 0),
)
return receipt_uuid
except Exception as parse_error:
- logger.debug(f'Ошибка парсинга времени чека: {parse_error}')
+ logger.debug('Ошибка парсинга времени чека', parse_error=parse_error)
continue
return None
except Exception as error:
- logger.warning(f'Ошибка проверки дубликата чека: {error}')
+ logger.warning('Ошибка проверки дубликата чека', error=error)
return None
async def get_incomes(
@@ -539,12 +553,12 @@ class NaloGoService:
# API возвращает структуру с полем content или items
incomes = result.get('content', result.get('items', []))
- logger.info(f'Получено {len(incomes)} доходов из NaloGO')
+ logger.info('Получено доходов из NaloGO', incomes_count=len(incomes))
return incomes
except Exception as error:
if self._is_service_unavailable(error):
- logger.warning(f'NaloGO временно недоступен: {error}')
+ logger.warning('NaloGO временно недоступен', error=error)
else:
- logger.error(f'Ошибка получения списка доходов: {error}', exc_info=True)
+ logger.error('Ошибка получения списка доходов', error=error, exc_info=True)
return None # None = ошибка, [] = нет чеков
diff --git a/app/services/notification_delivery_service.py b/app/services/notification_delivery_service.py
index d8ac6895..56f5d13f 100644
--- a/app/services/notification_delivery_service.py
+++ b/app/services/notification_delivery_service.py
@@ -7,17 +7,17 @@ This service handles notification delivery through appropriate channels:
"""
import asyncio
-import logging
from enum import Enum
from typing import Any
+import structlog
from aiogram import Bot
from app.config import settings
from app.database.models import User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class NotificationType(Enum):
@@ -163,23 +163,20 @@ class NotificationDeliveryService:
if email_sent or ws_sent:
logger.info(
- 'Уведомление %s отправлено email-пользователю %s (email=%s, ws=%s)',
- notification_type.value,
- user.id,
- email_sent,
- ws_sent,
+ 'Уведомление отправлено email-пользователю (email ws=)',
+ notification_type_value=notification_type.value,
+ user_id=user.id,
+ email_sent=email_sent,
+ ws_sent=ws_sent,
)
return True
logger.warning(
- 'Не удалось отправить уведомление %s email-пользователю %s',
- notification_type.value,
- user.id,
+ 'Не удалось отправить уведомление email-пользователю',
+ notification_type_value=notification_type.value,
+ user_id=user.id,
)
return False
- logger.debug(
- 'Пользователь %s не имеет telegram_id или verified email, пропускаем уведомление',
- user.id,
- )
+ logger.debug('Пользователь не имеет telegram_id или verified email, пропускаем уведомление', user_id=user.id)
return False
async def _send_telegram_notification(
@@ -193,17 +190,14 @@ class NotificationDeliveryService:
) -> bool:
"""Send notification via Telegram bot."""
if not bot:
- logger.warning(
- 'Bot instance not provided for Telegram notification to user %s',
- user.telegram_id,
- )
+ logger.warning('Bot instance not provided for Telegram notification to user', telegram_id=user.telegram_id)
return False
if not message:
logger.warning(
- 'No Telegram message provided for notification %s to user %s',
- notification_type.value,
- user.telegram_id,
+ 'No Telegram message provided for notification to user',
+ notification_type_value=notification_type.value,
+ telegram_id=user.telegram_id,
)
return False
@@ -222,32 +216,19 @@ class NotificationDeliveryService:
return True
except TimeoutError:
- logger.warning(
- 'Timeout при отправке Telegram уведомления пользователю %s',
- user.telegram_id,
- )
+ logger.warning('Timeout при отправке Telegram уведомления пользователю', telegram_id=user.telegram_id)
return False
except TelegramForbiddenError:
- logger.warning(
- 'Telegram user %s заблокировал бота',
- user.telegram_id,
- )
+ logger.warning('Telegram user заблокировал бота', telegram_id=user.telegram_id)
return False
except TelegramBadRequest as e:
- logger.warning(
- 'Ошибка отправки Telegram уведомления пользователю %s: %s',
- user.telegram_id,
- e,
- )
+ logger.warning('Ошибка отправки Telegram уведомления пользователю', telegram_id=user.telegram_id, e=e)
return False
except Exception as e:
- logger.error(
- 'Неожиданная ошибка при отправке Telegram уведомления: %s',
- e,
- )
+ logger.error('Неожиданная ошибка при отправке Telegram уведомления', e=e)
return False
async def _send_email_notification(
@@ -262,7 +243,7 @@ class NotificationDeliveryService:
return False
if not user.email or not user.email_verified:
- logger.debug('У пользователя %s нет подтверждённого email', user.id)
+ logger.debug('У пользователя нет подтверждённого email', user_id=user.id)
return False
try:
@@ -283,16 +264,13 @@ class NotificationDeliveryService:
'body_html': full_html,
}
except Exception as e:
- logger.debug('Не удалось проверить override шаблона: %s', e)
+ logger.debug('Не удалось проверить override шаблона', e=e)
if not template:
template = self.email_templates.get_template(notification_type, language, context)
if not template:
- logger.warning(
- 'Не найден email шаблон для %s',
- notification_type.value,
- )
+ logger.warning('Не найден email шаблон для', notification_type_value=notification_type.value)
return False
# Send email
@@ -305,20 +283,16 @@ class NotificationDeliveryService:
if success:
logger.info(
- 'Email уведомление %s отправлено пользователю %s (%s)',
- notification_type.value,
- user.id,
- user.email,
+ 'Email уведомление отправлено пользователю',
+ notification_type_value=notification_type.value,
+ user_id=user.id,
+ email=user.email,
)
return success
except Exception as e:
- logger.error(
- 'Ошибка отправки email уведомления пользователю %s: %s',
- user.id,
- e,
- )
+ logger.error('Ошибка отправки email уведомления пользователю', user_id=user.id, e=e)
return False
async def _send_websocket_notification(
@@ -338,11 +312,7 @@ class NotificationDeliveryService:
return True
except Exception as e:
- logger.debug(
- 'WebSocket уведомление не отправлено пользователю %s: %s',
- user.id,
- e,
- )
+ logger.debug('WebSocket уведомление не отправлено пользователю', user_id=user.id, e=e)
return False
# ============================================================================
diff --git a/app/services/notification_settings_service.py b/app/services/notification_settings_service.py
index 46082ad2..df0b4e2b 100644
--- a/app/services/notification_settings_service.py
+++ b/app/services/notification_settings_service.py
@@ -1,13 +1,14 @@
import json
-import logging
from copy import deepcopy
from pathlib import Path
from typing import Any
+import structlog
+
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class NotificationSettingsService:
@@ -38,7 +39,7 @@ class NotificationSettingsService:
try:
cls._storage_path.parent.mkdir(parents=True, exist_ok=True)
except Exception as exc: # pragma: no cover - filesystem guard
- logger.error('Failed to create notification settings dir: %s', exc)
+ logger.error('Failed to create notification settings dir', exc=exc)
@classmethod
def _load(cls) -> None:
@@ -53,7 +54,7 @@ class NotificationSettingsService:
else:
cls._data = {}
except Exception as exc:
- logger.error('Failed to load notification settings: %s', exc)
+ logger.error('Failed to load notification settings', exc=exc)
cls._data = {}
changed = cls._apply_defaults()
@@ -87,7 +88,7 @@ class NotificationSettingsService:
)
return True
except Exception as exc:
- logger.error('Failed to save notification settings: %s', exc)
+ logger.error('Failed to save notification settings', exc=exc)
return False
@classmethod
diff --git a/app/services/pal24_service.py b/app/services/pal24_service.py
index 67a63417..d14524ac 100644
--- a/app/services/pal24_service.py
+++ b/app/services/pal24_service.py
@@ -2,16 +2,17 @@
from __future__ import annotations
-import logging
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any
+import structlog
+
from app.config import settings
from app.external.pal24_client import Pal24APIError, Pal24Client
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class Pal24Service:
@@ -57,11 +58,11 @@ class Pal24Service:
filtered_payload = {k: v for k, v in extra_payload.items() if v not in (None, {})}
logger.info(
- 'Создаем Pal24 счет: user_id=%s, order_id=%s, amount=%s, ttl=%s',
- user_id,
- order_id,
- amount_decimal,
- ttl_seconds,
+ 'Создаем Pal24 счет: user_id order_id amount ttl',
+ user_id=user_id,
+ order_id=order_id,
+ amount_decimal=amount_decimal,
+ ttl_seconds=ttl_seconds,
)
response = await self.client.create_bill(
@@ -73,21 +74,21 @@ class Pal24Service:
**filtered_payload,
)
- logger.info('Pal24 счет создан: %s', response)
+ logger.info('Pal24 счет создан', response=response)
return response
async def get_bill_status(self, bill_id: str) -> dict[str, Any]:
- logger.debug('Запрашиваем статус Pal24 счета %s', bill_id)
+ logger.debug('Запрашиваем статус Pal24 счета', bill_id=bill_id)
return await self.client.get_bill_status(bill_id)
async def get_payment_status(self, payment_id: str) -> dict[str, Any]:
- logger.debug('Запрашиваем статус Pal24 платежа %s', payment_id)
+ logger.debug('Запрашиваем статус Pal24 платежа', payment_id=payment_id)
return await self.client.get_payment_status(payment_id)
async def get_bill_payments(self, bill_id: str) -> dict[str, Any]:
"""Возвращает список платежей, связанных со счетом."""
- logger.debug('Запрашиваем платежи Pal24 счёта %s', bill_id)
+ logger.debug('Запрашиваем платежи Pal24 счёта', bill_id=bill_id)
return await self.client.get_bill_payments(bill_id)
@staticmethod
@@ -105,10 +106,10 @@ class Pal24Service:
raise Pal24APIError('Pal24 callback signature mismatch')
logger.info(
- 'Получен Pal24 callback: InvId=%s, Status=%s, TrsId=%s',
- inv_id,
- payload.get('Status'),
- payload.get('TrsId'),
+ 'Получен Pal24 callback: InvId Status TrsId',
+ inv_id=inv_id,
+ payload=payload.get('Status'),
+ payload_2=payload.get('TrsId'),
)
return payload
diff --git a/app/services/partner_stats_service.py b/app/services/partner_stats_service.py
index 51e91001..2282645e 100644
--- a/app/services/partner_stats_service.py
+++ b/app/services/partner_stats_service.py
@@ -2,10 +2,10 @@
from __future__ import annotations
-import logging
from datetime import datetime, timedelta
from typing import Any
+import structlog
from sqlalchemy import and_, case, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -17,7 +17,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class PartnerStatsService:
diff --git a/app/services/payment/cloudpayments.py b/app/services/payment/cloudpayments.py
index 9c82a471..0f75c54c 100644
--- a/app/services/payment/cloudpayments.py
+++ b/app/services/payment/cloudpayments.py
@@ -53,17 +53,17 @@ class CloudPaymentsPaymentMixin:
if amount_kopeks < settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма CloudPayments меньше минимальной: %s < %s',
- amount_kopeks,
- settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS,
+ 'Сумма CloudPayments меньше минимальной: <',
+ amount_kopeks=amount_kopeks,
+ CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS=settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма CloudPayments больше максимальной: %s > %s',
- amount_kopeks,
- settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS,
+ 'Сумма CloudPayments больше максимальной: >',
+ amount_kopeks=amount_kopeks,
+ CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS=settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS,
)
return None
@@ -83,10 +83,10 @@ class CloudPaymentsPaymentMixin:
email=email,
)
except CloudPaymentsAPIError as error:
- logger.error('Ошибка создания CloudPayments платежа: %s', error)
+ logger.error('Ошибка создания CloudPayments платежа', error=error)
return None
except Exception as error:
- logger.exception('Непредвиденная ошибка при создании CloudPayments платежа: %s', error)
+ logger.exception('Непредвиденная ошибка при создании CloudPayments платежа', error=error)
return None
metadata = {
@@ -111,10 +111,10 @@ class CloudPaymentsPaymentMixin:
return None
logger.info(
- 'Создан CloudPayments платёж: invoice=%s, amount=%s₽, user=%s',
- invoice_id,
- amount_kopeks / 100,
- user_id,
+ 'Создан CloudPayments платёж: invoice amount=₽, user',
+ invoice_id=invoice_id,
+ amount_kopeks=amount_kopeks / 100,
+ user_id=user_id,
)
return {
@@ -156,10 +156,7 @@ class CloudPaymentsPaymentMixin:
payment = await payment_module.get_cloudpayments_payment_by_invoice_id(db, invoice_id)
if not payment:
- logger.warning(
- 'CloudPayments платёж не найден: invoice=%s, создаём новый',
- invoice_id,
- )
+ logger.warning('CloudPayments платёж не найден: invoice создаём новый', invoice_id=invoice_id)
# Try to extract user_id from account_id (we now use user_id as AccountId)
try:
user_id = int(account_id) if account_id else None
@@ -167,7 +164,7 @@ class CloudPaymentsPaymentMixin:
user_id = None
if not user_id:
- logger.error('Не удалось определить user_id из account_id: %s', account_id)
+ logger.error('Не удалось определить user_id из account_id', account_id=account_id)
return False
# Get user by ID
@@ -175,7 +172,7 @@ class CloudPaymentsPaymentMixin:
user = await get_user_by_id(db, user_id)
if not user:
- logger.error('Пользователь не найден: id=%s', user_id)
+ logger.error('Пользователь не найден: id', user_id=user_id)
return False
# Create payment record
@@ -194,7 +191,7 @@ class CloudPaymentsPaymentMixin:
# Check if already processed
if payment.is_paid:
- logger.info('CloudPayments платёж уже обработан: invoice=%s', invoice_id)
+ logger.info('CloudPayments платёж уже обработан: invoice', invoice_id=invoice_id)
return True
# Update payment record
@@ -219,7 +216,7 @@ class CloudPaymentsPaymentMixin:
user = await get_user_by_id(db, payment.user_id)
if not user:
- logger.error('Пользователь не найден: id=%s', payment.user_id)
+ logger.error('Пользователь не найден: id', user_id=payment.user_id)
return False
# Add balance (без автоматической транзакции - создадим ниже с external_id)
@@ -245,10 +242,10 @@ class CloudPaymentsPaymentMixin:
user_id_display = user.telegram_id or user.email or f'#{user.id}'
logger.info(
- 'CloudPayments платёж успешно обработан: invoice=%s, amount=%s₽, user=%s',
- invoice_id,
- amount_kopeks / 100,
- user_id_display,
+ 'CloudPayments платёж успешно обработан: invoice amount=₽, user',
+ invoice_id=invoice_id,
+ amount_kopeks=amount_kopeks / 100,
+ user_id_display=user_id_display,
)
# Send notification to user
@@ -259,13 +256,13 @@ class CloudPaymentsPaymentMixin:
transaction=transaction,
)
except Exception as error:
- logger.exception('Ошибка отправки уведомления CloudPayments: %s', error)
+ logger.exception('Ошибка отправки уведомления CloudPayments', error=error)
# Auto-purchase if enabled
try:
await auto_purchase_saved_cart_after_topup(db, user, bot=getattr(self, 'bot', None))
except Exception as error:
- logger.exception('Ошибка автопокупки после CloudPayments: %s', error)
+ logger.exception('Ошибка автопокупки после CloudPayments', error=error)
return True
@@ -305,10 +302,10 @@ class CloudPaymentsPaymentMixin:
await db.commit()
logger.info(
- 'CloudPayments платёж неуспешен: invoice=%s, reason=%s (code=%s)',
- invoice_id,
- reason,
- reason_code,
+ 'CloudPayments платёж неуспешен: invoice reason= (code=)',
+ invoice_id=invoice_id,
+ reason=reason,
+ reason_code=reason_code,
)
# Notify user about failed payment (account_id now contains user_id, not telegram_id)
@@ -328,7 +325,7 @@ class CloudPaymentsPaymentMixin:
message=card_holder_message,
)
except Exception as error:
- logger.exception('Ошибка отправки уведомления о неуспешном платеже: %s', error)
+ logger.exception('Ошибка отправки уведомления о неуспешном платеже', error=error)
return True
@@ -353,7 +350,7 @@ class CloudPaymentsPaymentMixin:
# Skip email-only users (no telegram_id)
if not user.telegram_id:
- logger.debug('Skipping CloudPayments notification for email-only user %s', user.id)
+ logger.debug('Skipping CloudPayments notification for email-only user', user_id=user.id)
return
texts = get_texts(user.language)
@@ -387,7 +384,7 @@ class CloudPaymentsPaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.warning('Не удалось отправить уведомление пользователю %s: %s', user.telegram_id, error)
+ logger.warning('Не удалось отправить уведомление пользователю', telegram_id=user.telegram_id, error=error)
async def _send_cloudpayments_fail_notification(
self,
@@ -415,7 +412,7 @@ class CloudPaymentsPaymentMixin:
parse_mode='HTML',
)
except Exception as error:
- logger.warning('Не удалось отправить уведомление пользователю %s: %s', telegram_id, error)
+ logger.warning('Не удалось отправить уведомление пользователю', telegram_id=telegram_id, error=error)
async def get_cloudpayments_payment_status(
self,
@@ -437,7 +434,7 @@ class CloudPaymentsPaymentMixin:
# Get local payment record
payment = await payment_module.get_cloudpayments_payment_by_id(db, local_payment_id)
if not payment:
- logger.warning('CloudPayments payment not found: id=%s', local_payment_id)
+ logger.warning('CloudPayments payment not found: id', local_payment_id=local_payment_id)
return None
# If already paid, return current state
@@ -454,10 +451,7 @@ class CloudPaymentsPaymentMixin:
api_response = await self.cloudpayments_service.find_payment(payment.invoice_id)
if not api_response.get('Success'):
- logger.debug(
- 'CloudPayments API: payment not found or error for invoice=%s',
- payment.invoice_id,
- )
+ logger.debug('CloudPayments API: payment not found or error for invoice', invoice_id=payment.invoice_id)
return {'payment': payment, 'status': payment.status}
model = api_response.get('Model', {})
@@ -493,8 +487,6 @@ class CloudPaymentsPaymentMixin:
except Exception as error:
logger.error(
- 'Error checking CloudPayments payment status: id=%s, error=%s',
- local_payment_id,
- error,
+ 'Error checking CloudPayments payment status: id error', local_payment_id=local_payment_id, error=error
)
return {'payment': payment, 'status': payment.status}
diff --git a/app/services/payment/common.py b/app/services/payment/common.py
index e9969c13..5e2cff06 100644
--- a/app/services/payment/common.py
+++ b/app/services/payment/common.py
@@ -65,15 +65,15 @@ class PaymentCommonMixin:
has_active_subscription = bool(is_active and not row.is_trial)
except Exception as db_error:
logger.warning(
- 'Не удалось загрузить подписку пользователя %s из БД: %s',
- getattr(user, 'id', None),
- db_error,
+ 'Не удалось загрузить подписку пользователя из БД',
+ getattr=getattr(user, 'id', None),
+ db_error=db_error,
)
except Exception as error: # pragma: no cover - защитный код
logger.error(
- 'Ошибка загрузки подписки пользователя %s при построении клавиатуры после пополнения: %s',
- getattr(user, 'id', None),
- error,
+ 'Ошибка загрузки подписки пользователя при построении клавиатуры после пополнения',
+ getattr=getattr(user, 'id', None),
+ error=error,
)
# Создаем основную кнопку: если есть активная подписка - продлить, иначе купить
@@ -92,9 +92,9 @@ class PaymentCommonMixin:
has_saved_cart = await user_cart_service.has_user_cart(user.id)
except Exception as cart_error:
logger.warning(
- 'Не удалось проверить наличие сохраненной корзины у пользователя %s: %s',
- user.id,
- cart_error,
+ 'Не удалось проверить наличие сохраненной корзины у пользователя',
+ user_id=user.id,
+ cart_error=cart_error,
)
has_saved_cart = False
@@ -166,9 +166,9 @@ class PaymentCommonMixin:
)
except Exception as ws_error:
logger.warning(
- 'Не удалось отправить WS уведомление о пополнении баланса для user_id=%s: %s',
- user_id,
- ws_error,
+ 'Не удалось отправить WS уведомление о пополнении баланса для user_id',
+ user_id=user_id,
+ ws_error=ws_error,
)
if not getattr(self, 'bot', None):
@@ -208,11 +208,7 @@ class PaymentCommonMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.error(
- 'Ошибка отправки уведомления пользователю %s: %s',
- telegram_id,
- error,
- )
+ logger.error('Ошибка отправки уведомления пользователю', telegram_id=telegram_id, error=error)
async def _ensure_user_snapshot(
self,
@@ -260,9 +256,9 @@ class PaymentCommonMixin:
return _build_snapshot(fetched_user)
except Exception as fetch_error:
logger.warning(
- 'Не удалось обновить пользователя %s из переданной сессии: %s',
- telegram_id,
- fetch_error,
+ 'Не удалось обновить пользователя из переданной сессии',
+ telegram_id=telegram_id,
+ fetch_error=fetch_error,
)
try:
@@ -271,9 +267,7 @@ class PaymentCommonMixin:
return _build_snapshot(fetched_user)
except Exception as fetch_error:
logger.warning(
- 'Не удалось получить пользователя %s для уведомления: %s',
- telegram_id,
- fetch_error,
+ 'Не удалось получить пользователя для уведомления', telegram_id=telegram_id, fetch_error=fetch_error
)
return None
@@ -288,13 +282,13 @@ class PaymentCommonMixin:
"""Общая точка учёта успешных платежей (используется провайдерами при необходимости)."""
try:
logger.info(
- 'Обработан успешный платеж: %s, %s₽, пользователь %s, метод %s',
- payment_id,
- amount_kopeks / 100,
- user_id,
- payment_method,
+ 'Обработан успешный платеж ₽, пользователь , метод',
+ payment_id=payment_id,
+ amount_kopeks=amount_kopeks / 100,
+ user_id=user_id,
+ payment_method=payment_method,
)
return True
except Exception as error:
- logger.error('Ошибка обработки платежа %s: %s', payment_id, error)
+ logger.error('Ошибка обработки платежа', payment_id=payment_id, error=error)
return False
diff --git a/app/services/payment/cryptobot.py b/app/services/payment/cryptobot.py
index 01ba3eaf..2c395450 100644
--- a/app/services/payment/cryptobot.py
+++ b/app/services/payment/cryptobot.py
@@ -109,11 +109,11 @@ class CryptoBotPaymentMixin:
)
logger.info(
- 'Создан CryptoBot платеж %s на %s %s для пользователя %s',
- invoice_data['invoice_id'],
- amount_str,
- asset,
- user_id,
+ 'Создан CryptoBot платеж на для пользователя',
+ invoice_data=invoice_data['invoice_id'],
+ amount_str=amount_str,
+ asset=asset,
+ user_id=user_id,
)
return {
@@ -129,7 +129,7 @@ class CryptoBotPaymentMixin:
}
except Exception as error:
- logger.error('Ошибка создания CryptoBot платежа: %s', error)
+ logger.error('Ошибка создания CryptoBot платежа', error=error)
return None
async def process_cryptobot_webhook(
@@ -142,7 +142,7 @@ class CryptoBotPaymentMixin:
update_type = webhook_data.get('update_type')
if update_type != 'invoice_paid':
- logger.info('Пропуск CryptoBot webhook с типом: %s', update_type)
+ logger.info('Пропуск CryptoBot webhook с типом', update_type=update_type)
return True
payload = webhook_data.get('payload', {})
@@ -157,12 +157,12 @@ class CryptoBotPaymentMixin:
payment = await cryptobot_crud.get_cryptobot_payment_by_invoice_id(db, invoice_id)
if not payment:
logger.warning(
- 'CryptoBot платеж не найден в БД: %s (возвращаем 200 чтобы остановить ретраи)', invoice_id
+ 'CryptoBot платеж не найден в БД: (возвращаем 200 чтобы остановить ретраи)', invoice_id=invoice_id
)
return True
if payment.status == 'paid':
- logger.info('CryptoBot платеж %s уже обработан', invoice_id)
+ logger.info('CryptoBot платеж уже обработан', invoice_id=invoice_id)
return True
paid_at_str = payload.get('paid_at')
@@ -215,17 +215,15 @@ class CryptoBotPaymentMixin:
amount_kopeks = int(amount_rubles_rounded * 100)
conversion_rate = amount_rubles / amount_usd if amount_usd > 0 else 0
logger.info(
- 'Конвертация USD->RUB: $%s -> %s₽ (округлено до %s₽, курс: %.2f)',
- amount_usd,
- amount_rubles,
- amount_rubles_rounded,
- conversion_rate,
+ 'Конвертация USD->RUB: $ -> ₽ (округлено до ₽, курс:)',
+ amount_usd=amount_usd,
+ amount_rubles=amount_rubles,
+ amount_rubles_rounded=amount_rubles_rounded,
+ conversion_rate=conversion_rate,
)
except Exception as error:
logger.warning(
- 'Ошибка конвертации валют для платежа %s, используем курс 1:1: %s',
- invoice_id,
- error,
+ 'Ошибка конвертации валют для платежа , используем курс 1:1', invoice_id=invoice_id, error=error
)
amount_rubles = amount_usd
amount_rubles_rounded = math.ceil(amount_rubles)
@@ -234,9 +232,9 @@ class CryptoBotPaymentMixin:
if amount_kopeks <= 0:
logger.error(
- 'Некорректная сумма после конвертации: %s копеек для платежа %s',
- amount_kopeks,
- invoice_id,
+ 'Некорректная сумма после конвертации: копеек для платежа',
+ amount_kopeks=amount_kopeks,
+ invoice_id=invoice_id,
)
return False
@@ -261,10 +259,7 @@ class CryptoBotPaymentMixin:
get_user_by_id = payment_service_module.get_user_by_id
user = await get_user_by_id(db, updated_payment.user_id)
if not user:
- logger.error(
- 'Пользователь с ID %s не найден при пополнении баланса',
- updated_payment.user_id,
- )
+ logger.error('Пользователь с ID не найден при пополнении баланса', user_id=updated_payment.user_id)
return False
old_balance = user.balance_kopeks
@@ -288,10 +283,7 @@ class CryptoBotPaymentMixin:
getattr(self, 'bot', None),
)
except Exception as error:
- logger.error(
- 'Ошибка обработки реферального пополнения CryptoBot: %s',
- error,
- )
+ logger.error('Ошибка обработки реферального пополнения CryptoBot', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -332,10 +324,7 @@ class CryptoBotPaymentMixin:
asset=updated_payment.asset,
)
except Exception as error:
- logger.error(
- 'Ошибка подготовки уведомления о пополнении CryptoBot: %s',
- error,
- )
+ logger.error('Ошибка подготовки уведомления о пополнении CryptoBot', error=error)
# Проверяем наличие сохраненной корзины для возврата к оформлению подписки
try:
@@ -354,9 +343,9 @@ class CryptoBotPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -397,9 +386,9 @@ class CryptoBotPaymentMixin:
)
except Exception as error:
logger.error(
- 'Ошибка при работе с сохраненной корзиной для пользователя %s: %s',
- user.id,
- error,
+ 'Ошибка при работе с сохраненной корзиной для пользователя',
+ user_id=user.id,
+ error=error,
exc_info=True,
)
@@ -415,7 +404,7 @@ class CryptoBotPaymentMixin:
return True
except Exception as error:
- logger.error('Ошибка обработки CryptoBot webhook: %s', error, exc_info=True)
+ logger.error('Ошибка обработки CryptoBot webhook', error=error, exc_info=True)
return False
async def _process_subscription_renewal_payment(
@@ -430,25 +419,25 @@ class CryptoBotPaymentMixin:
user = await payment_service_module.get_user_by_id(db, payment.user_id)
except Exception as error:
logger.error(
- 'Не удалось загрузить пользователя %s для продления через CryptoBot: %s',
- getattr(payment, 'user_id', None),
- error,
+ 'Не удалось загрузить пользователя для продления через CryptoBot',
+ getattr=getattr(payment, 'user_id', None),
+ error=error,
)
return False
if not user:
logger.error(
- 'Пользователь %s не найден при обработке продления через CryptoBot',
- getattr(payment, 'user_id', None),
+ 'Пользователь не найден при обработке продления через CryptoBot',
+ getattr=getattr(payment, 'user_id', None),
)
return False
subscription = getattr(user, 'subscription', None)
if not subscription or subscription.id != descriptor.subscription_id:
logger.warning(
- 'Продление через CryptoBot отклонено: подписка %s не совпадает с ожидаемой %s',
- getattr(subscription, 'id', None),
- descriptor.subscription_id,
+ 'Продление через CryptoBot отклонено: подписка не совпадает с ожидаемой',
+ getattr=getattr(subscription, 'id', None),
+ subscription_id=descriptor.subscription_id,
)
return False
@@ -458,9 +447,9 @@ class CryptoBotPaymentMixin:
pricing_model = SubscriptionRenewalPricing.from_payload(descriptor.pricing_snapshot)
except Exception as error:
logger.warning(
- 'Не удалось восстановить сохраненную стоимость продления из payload %s: %s',
- payment.invoice_id,
- error,
+ 'Не удалось восстановить сохраненную стоимость продления из payload',
+ invoice_id=payment.invoice_id,
+ error=error,
)
if pricing_model is None:
@@ -473,18 +462,18 @@ class CryptoBotPaymentMixin:
)
except Exception as error:
logger.error(
- 'Не удалось пересчитать стоимость продления для CryptoBot %s: %s',
- payment.invoice_id,
- error,
+ 'Не удалось пересчитать стоимость продления для CryptoBot',
+ invoice_id=payment.invoice_id,
+ error=error,
)
return False
if pricing_model.final_total != descriptor.total_amount_kopeks:
logger.warning(
- 'Сумма продления через CryptoBot %s изменилась (ожидалось %s, получено %s)',
- payment.invoice_id,
- descriptor.total_amount_kopeks,
- pricing_model.final_total,
+ 'Сумма продления через CryptoBot изменилась (ожидалось , получено)',
+ invoice_id=payment.invoice_id,
+ total_amount_kopeks=descriptor.total_amount_kopeks,
+ final_total=pricing_model.final_total,
)
pricing_model.final_total = descriptor.total_amount_kopeks
pricing_model.per_month = (
@@ -507,10 +496,10 @@ class CryptoBotPaymentMixin:
current_balance = getattr(user, 'balance_kopeks', 0)
if current_balance < required_balance:
logger.warning(
- 'Недостаточно средств на балансе пользователя %s для завершения продления: нужно %s, доступно %s',
- user.id,
- required_balance,
- current_balance,
+ 'Недостаточно средств на балансе пользователя для завершения продления: нужно , доступно',
+ user_id=user.id,
+ required_balance=required_balance,
+ current_balance=current_balance,
)
return False
@@ -528,17 +517,14 @@ class CryptoBotPaymentMixin:
)
except SubscriptionRenewalChargeError as error:
logger.error(
- 'Списание баланса не выполнено при продлении через CryptoBot %s: %s',
- payment.invoice_id,
- error,
+ 'Списание баланса не выполнено при продлении через CryptoBot',
+ invoice_id=payment.invoice_id,
+ error=error,
)
return False
except Exception as error:
logger.error(
- 'Ошибка завершения продления через CryptoBot %s: %s',
- payment.invoice_id,
- error,
- exc_info=True,
+ 'Ошибка завершения продления через CryptoBot', invoice_id=payment.invoice_id, error=error, exc_info=True
)
return False
@@ -552,21 +538,21 @@ class CryptoBotPaymentMixin:
)
except Exception as error:
logger.warning(
- 'Не удалось связать платеж CryptoBot %s с транзакцией %s: %s',
- payment.invoice_id,
- transaction.id,
- error,
+ 'Не удалось связать платеж CryptoBot с транзакцией',
+ invoice_id=payment.invoice_id,
+ transaction_id=transaction.id,
+ error=error,
)
external_amount_label = settings.format_price(descriptor.missing_amount_kopeks)
balance_amount_label = settings.format_price(required_balance)
logger.info(
- 'Подписка %s продлена через CryptoBot invoice %s (внешний платеж %s, списано с баланса %s)',
- subscription.id,
- payment.invoice_id,
- external_amount_label,
- balance_amount_label,
+ 'Подписка продлена через CryptoBot invoice (внешний платеж , списано с баланса)',
+ subscription_id=subscription.id,
+ invoice_id=payment.invoice_id,
+ external_amount_label=external_amount_label,
+ balance_amount_label=balance_amount_label,
)
return True
@@ -582,9 +568,7 @@ class CryptoBotPaymentMixin:
from app.services.admin_notification_service import AdminNotificationService
except Exception as error:
logger.error(
- 'Не удалось импортировать зависимости для админ-уведомления CryptoBot: %s',
- error,
- exc_info=True,
+ 'Не удалось импортировать зависимости для админ-уведомления CryptoBot', error=error, exc_info=True
)
return
@@ -593,19 +577,15 @@ class CryptoBotPaymentMixin:
user = await get_user_by_id(session, context.user_id)
transaction = await get_transaction_by_id(session, context.transaction_id)
except Exception as error:
- logger.error(
- 'Ошибка загрузки данных для админ-уведомления CryptoBot: %s',
- error,
- exc_info=True,
- )
+ logger.error('Ошибка загрузки данных для админ-уведомления CryptoBot', error=error, exc_info=True)
await session.rollback()
return
if not user or not transaction:
logger.warning(
- 'Пропущена отправка админ-уведомления CryptoBot: user=%s transaction=%s',
- bool(user),
- bool(transaction),
+ 'Пропущена отправка админ-уведомления CryptoBot: user= transaction',
+ user=bool(user),
+ transaction=bool(transaction),
)
return
@@ -622,11 +602,7 @@ class CryptoBotPaymentMixin:
db=session,
)
except Exception as error:
- logger.error(
- 'Ошибка отправки админ-уведомления о пополнении CryptoBot: %s',
- error,
- exc_info=True,
- )
+ logger.error('Ошибка отправки админ-уведомления о пополнении CryptoBot', error=error, exc_info=True)
async def _deliver_user_topup_notification(self, payload: _UserNotificationPayload) -> None:
bot_instance = getattr(self, 'bot', None)
@@ -652,10 +628,7 @@ class CryptoBotPaymentMixin:
payload.asset,
)
except Exception as error:
- logger.error(
- 'Ошибка отправки уведомления о пополнении CryptoBot: %s',
- error,
- )
+ logger.error('Ошибка отправки уведомления о пополнении CryptoBot', error=error)
async def _deliver_saved_cart_reminder(self, payload: _SavedCartNotificationPayload) -> None:
bot_instance = getattr(self, 'bot', None)
@@ -674,14 +647,13 @@ class CryptoBotPaymentMixin:
reply_markup=payload.reply_markup,
)
logger.info(
- 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю %s',
- payload.user_id,
+ 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю', user_id=payload.user_id
)
except Exception as error:
logger.error(
- 'Ошибка отправки уведомления о сохраненной корзине для пользователя %s: %s',
- payload.user_id,
- error,
+ 'Ошибка отправки уведомления о сохраненной корзине для пользователя',
+ user_id=payload.user_id,
+ error=error,
exc_info=True,
)
@@ -695,7 +667,7 @@ class CryptoBotPaymentMixin:
cryptobot_crud = import_module('app.database.crud.cryptobot')
payment = await cryptobot_crud.get_cryptobot_payment_by_id(db, local_payment_id)
if not payment:
- logger.warning('CryptoBot платеж %s не найден', local_payment_id)
+ logger.warning('CryptoBot платеж не найден', local_payment_id=local_payment_id)
return None
if not self.cryptobot_service:
@@ -706,11 +678,7 @@ class CryptoBotPaymentMixin:
try:
invoices = await self.cryptobot_service.get_invoices(invoice_ids=[invoice_id])
except Exception as error: # pragma: no cover - network errors
- logger.error(
- 'Ошибка запроса статуса CryptoBot invoice %s: %s',
- invoice_id,
- error,
- )
+ logger.error('Ошибка запроса статуса CryptoBot invoice', invoice_id=invoice_id, error=error)
return {'payment': payment}
remote_invoice: dict[str, Any] | None = None
@@ -721,10 +689,7 @@ class CryptoBotPaymentMixin:
break
if not remote_invoice:
- logger.info(
- 'CryptoBot invoice %s не найден через API при ручной проверке',
- invoice_id,
- )
+ logger.info('CryptoBot invoice не найден через API при ручной проверке', invoice_id=invoice_id)
refreshed = await cryptobot_crud.get_cryptobot_payment_by_id(db, local_payment_id)
return {'payment': refreshed or payment}
diff --git a/app/services/payment/freekassa.py b/app/services/payment/freekassa.py
index d8974dfe..10683c54 100644
--- a/app/services/payment/freekassa.py
+++ b/app/services/payment/freekassa.py
@@ -54,17 +54,17 @@ class FreekassaPaymentMixin:
# Валидация лимитов
if amount_kopeks < settings.FREEKASSA_MIN_AMOUNT_KOPEKS:
logger.warning(
- 'Freekassa: сумма %s меньше минимальной %s',
- amount_kopeks,
- settings.FREEKASSA_MIN_AMOUNT_KOPEKS,
+ 'Freekassa: сумма меньше минимальной',
+ amount_kopeks=amount_kopeks,
+ FREEKASSA_MIN_AMOUNT_KOPEKS=settings.FREEKASSA_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.FREEKASSA_MAX_AMOUNT_KOPEKS:
logger.warning(
- 'Freekassa: сумма %s больше максимальной %s',
- amount_kopeks,
- settings.FREEKASSA_MAX_AMOUNT_KOPEKS,
+ 'Freekassa: сумма больше максимальной',
+ amount_kopeks=amount_kopeks,
+ FREEKASSA_MAX_AMOUNT_KOPEKS=settings.FREEKASSA_MAX_AMOUNT_KOPEKS,
)
return None
@@ -96,11 +96,7 @@ class FreekassaPaymentMixin:
email=email,
payment_system_id=settings.FREEKASSA_PAYMENT_SYSTEM_ID,
)
- logger.info(
- 'Freekassa API: создан заказ order_id=%s, url=%s',
- order_id,
- payment_url,
- )
+ logger.info('Freekassa API: создан заказ order_id url', order_id=order_id, payment_url=payment_url)
else:
# Генерируем URL для формы оплаты (стандартный способ)
payment_url = freekassa_service.build_payment_url(
@@ -128,12 +124,12 @@ class FreekassaPaymentMixin:
)
logger.info(
- 'Freekassa: создан платеж order_id=%s, user_id=%s, amount=%s %s, use_api=%s',
- order_id,
- user_id,
- amount_rubles,
- currency,
- settings.FREEKASSA_USE_API,
+ 'Freekassa: создан платеж order_id user_id amount use_api',
+ order_id=order_id,
+ user_id=user_id,
+ amount_rubles=amount_rubles,
+ currency=currency,
+ FREEKASSA_USE_API=settings.FREEKASSA_USE_API,
)
return {
@@ -147,7 +143,7 @@ class FreekassaPaymentMixin:
}
except Exception as e:
- logger.exception('Freekassa: ошибка создания платежа: %s', e)
+ logger.exception('Freekassa: ошибка создания платежа', e=e)
return None
async def process_freekassa_webhook(
@@ -181,12 +177,12 @@ class FreekassaPaymentMixin:
try:
# Проверка IP
if not freekassa_service.verify_webhook_ip(client_ip):
- logger.warning('Freekassa webhook: недоверенный IP %s', client_ip)
+ logger.warning('Freekassa webhook: недоверенный IP', client_ip=client_ip)
return False
# Проверка подписи
if not freekassa_service.verify_webhook_signature(merchant_id, amount, order_id, sign):
- logger.warning('Freekassa webhook: неверная подпись для order_id=%s', order_id)
+ logger.warning('Freekassa webhook: неверная подпись для order_id', order_id=order_id)
return False
# Импортируем CRUD модуль
@@ -195,21 +191,21 @@ class FreekassaPaymentMixin:
# Получаем платеж из БД
payment = await freekassa_crud.get_freekassa_payment_by_order_id(db, order_id)
if not payment:
- logger.warning('Freekassa webhook: платеж не найден order_id=%s', order_id)
+ logger.warning('Freekassa webhook: платеж не найден order_id', order_id=order_id)
return False
# Проверка дублирования
if payment.is_paid:
- logger.info('Freekassa webhook: платеж уже обработан order_id=%s', order_id)
+ logger.info('Freekassa webhook: платеж уже обработан order_id', order_id=order_id)
return True
# Проверка суммы
expected_amount = payment.amount_kopeks / 100
if abs(amount - expected_amount) > 0.01:
logger.warning(
- 'Freekassa webhook: несоответствие суммы ожидалось=%s, получено=%s',
- expected_amount,
- amount,
+ 'Freekassa webhook: несоответствие суммы ожидалось получено',
+ expected_amount=expected_amount,
+ amount=amount,
)
return False
@@ -236,7 +232,7 @@ class FreekassaPaymentMixin:
return await self._finalize_freekassa_payment(db, payment, intid=intid, trigger='webhook')
except Exception as e:
- logger.exception('Freekassa webhook: ошибка обработки: %s', e)
+ logger.exception('Freekassa webhook: ошибка обработки', e=e)
return False
async def _finalize_freekassa_payment(
@@ -252,9 +248,7 @@ class FreekassaPaymentMixin:
if payment.transaction_id:
logger.info(
- 'Freekassa платеж %s уже привязан к транзакции (trigger=%s)',
- payment.order_id,
- trigger,
+ 'Freekassa платеж уже привязан к транзакции (trigger=)', order_id=payment.order_id, trigger=trigger
)
return True
@@ -262,10 +256,10 @@ class FreekassaPaymentMixin:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
- 'Пользователь %s не найден для Freekassa платежа %s (trigger=%s)',
- payment.user_id,
- payment.order_id,
- trigger,
+ 'Пользователь не найден для Freekassa платежа (trigger=)',
+ user_id=payment.user_id,
+ order_id=payment.order_id,
+ trigger=trigger,
)
return False
@@ -311,7 +305,7 @@ class FreekassaPaymentMixin:
await process_referral_topup(db, user.id, payment.amount_kopeks, getattr(self, 'bot', None))
except Exception as error:
- logger.error('Ошибка обработки реферального пополнения Freekassa: %s', error)
+ logger.error('Ошибка обработки реферального пополнения Freekassa', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -339,7 +333,7 @@ class FreekassaPaymentMixin:
db=db,
)
except Exception as error:
- logger.error('Ошибка отправки админ уведомления Freekassa: %s', error)
+ logger.error('Ошибка отправки админ уведомления Freekassa', error=error)
# Отправка уведомления пользователю
if getattr(self, 'bot', None) and user.telegram_id:
@@ -359,7 +353,7 @@ class FreekassaPaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.error('Ошибка отправки уведомления пользователю Freekassa: %s', error)
+ logger.error('Ошибка отправки уведомления пользователю Freekassa', error=error)
# Автопокупка подписки
try:
@@ -379,9 +373,9 @@ class FreekassaPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -424,17 +418,14 @@ class FreekassaPaymentMixin:
)
except Exception as error:
logger.error(
- 'Ошибка при работе с сохраненной корзиной для пользователя %s: %s',
- user.id,
- error,
- exc_info=True,
+ 'Ошибка при работе с сохраненной корзиной для пользователя', user_id=user.id, error=error, exc_info=True
)
logger.info(
- '✅ Обработан Freekassa платеж %s для пользователя %s (trigger=%s)',
- payment.order_id,
- payment.user_id,
- trigger,
+ '✅ Обработан Freekassa платеж для пользователя (trigger=)',
+ order_id=payment.order_id,
+ user_id=payment.user_id,
+ trigger=trigger,
)
return True
@@ -458,7 +449,7 @@ class FreekassaPaymentMixin:
status_data = await freekassa_service.get_order_status(order_id)
return status_data
except Exception as e:
- logger.exception('Freekassa: ошибка проверки статуса: %s', e)
+ logger.exception('Freekassa: ошибка проверки статуса', e=e)
return None
async def get_freekassa_payment_status(
@@ -473,7 +464,7 @@ class FreekassaPaymentMixin:
payment = await freekassa_crud.get_freekassa_payment_by_id(db, local_payment_id)
if not payment:
- logger.warning('Freekassa payment not found: id=%s', local_payment_id)
+ logger.warning('Freekassa payment not found: id', local_payment_id=local_payment_id)
return None
if payment.is_paid:
@@ -512,7 +503,7 @@ class FreekassaPaymentMixin:
fk_status = int(target_order.get('status', 0))
if fk_status == 1:
- logger.info('Freekassa payment %s confirmed via API', payment.order_id)
+ logger.info('Freekassa payment confirmed via API', order_id=payment.order_id)
callback_payload = {
'check_source': 'api',
@@ -541,7 +532,7 @@ class FreekassaPaymentMixin:
trigger='api_check',
)
except Exception as e:
- logger.error('Error checking Freekassa payment status: %s', e)
+ logger.error('Error checking Freekassa payment status', e=e)
return {
'payment': payment,
diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py
index 0d77e14a..605b1a8b 100644
--- a/app/services/payment/heleket.py
+++ b/app/services/payment/heleket.py
@@ -36,7 +36,7 @@ class HeleketPaymentMixin:
return None
if amount_kopeks <= 0:
- logger.error('Сумма Heleket должна быть положительной: %s', amount_kopeks)
+ logger.error('Сумма Heleket должна быть положительной', amount_kopeks=amount_kopeks)
return None
amount_rubles = amount_kopeks / 100
@@ -52,7 +52,7 @@ class HeleketPaymentMixin:
if rounded != 0:
discount_percent = -rounded
except (TypeError, ValueError):
- logger.warning('Некорректная наценка Heleket: %s', markup_percent)
+ logger.warning('Некорректная наценка Heleket', markup_percent=markup_percent)
payload: dict[str, Any] = {
'amount': amount_str,
@@ -89,7 +89,7 @@ class HeleketPaymentMixin:
try:
response = await self.heleket_service.create_payment(payload) # type: ignore[union-attr]
except Exception as error: # pragma: no cover - safety net
- logger.exception('Ошибка создания Heleket платежа: %s', error)
+ logger.exception('Ошибка создания Heleket платежа', error=error)
return None
if not response:
@@ -98,7 +98,7 @@ class HeleketPaymentMixin:
payment_result = response.get('result') if isinstance(response, dict) else None
if not payment_result:
- logger.error('Некорректный ответ Heleket API: %s', response)
+ logger.error('Некорректный ответ Heleket API', response=response)
return None
uuid = str(payment_result.get('uuid'))
@@ -150,12 +150,7 @@ class HeleketPaymentMixin:
metadata={'raw_response': payment_result, **metadata},
)
- logger.info(
- 'Создан Heleket платеж %s на %s₽ для пользователя %s',
- uuid,
- amount_str,
- user_id,
- )
+ logger.info('Создан Heleket платеж на ₽ для пользователя', uuid=uuid, amount_str=amount_str, user_id=user_id)
return {
'local_payment_id': local_payment.id,
@@ -179,7 +174,7 @@ class HeleketPaymentMixin:
metadata_key: str,
) -> HeleketPayment | None:
if not isinstance(payload, dict):
- logger.error('Heleket webhook payload не является словарём: %s', payload)
+ logger.error('Heleket webhook payload не является словарём', payload=payload)
return None
heleket_crud = import_module('app.database.crud.heleket')
@@ -190,7 +185,7 @@ class HeleketPaymentMixin:
status = payload.get('status') or payload.get('payment_status')
if not uuid and not order_id:
- logger.error('Heleket webhook без uuid/order_id: %s', payload)
+ logger.error('Heleket webhook без uuid/order_id', payload=payload)
return None
payment = None
@@ -200,11 +195,7 @@ class HeleketPaymentMixin:
payment = await heleket_crud.get_heleket_payment_by_order_id(db, order_id)
if not payment:
- logger.error(
- 'Heleket платеж не найден (uuid=%s order_id=%s)',
- uuid,
- order_id,
- )
+ logger.error('Heleket платеж не найден (uuid= order_id=)', uuid=uuid, order_id=order_id)
return None
payer_amount = payload.get('payer_amount') or payload.get('payment_amount')
@@ -270,11 +261,7 @@ class HeleketPaymentMixin:
try:
await self.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - depends on rights
- logger.warning(
- 'Не удалось удалить счёт Heleket %s: %s',
- message_id,
- delete_error,
- )
+ logger.warning('Не удалось удалить счёт Heleket', message_id=message_id, delete_error=delete_error)
else:
metadata.pop('invoice_message', None)
invoice_message_removed = True
@@ -290,26 +277,25 @@ class HeleketPaymentMixin:
)
updated_payment.metadata_json = metadata
except Exception as error: # pragma: no cover - diagnostics
- logger.warning(
- 'Не удалось обновить метаданные Heleket после удаления счёта: %s',
- error,
- )
+ logger.warning('Не удалось обновить метаданные Heleket после удаления счёта', error=error)
if updated_payment.transaction_id:
logger.info(
- 'Heleket платеж %s уже связан с транзакцией %s',
- updated_payment.uuid,
- updated_payment.transaction_id,
+ 'Heleket платеж уже связан с транзакцией',
+ uuid=updated_payment.uuid,
+ transaction_id=updated_payment.transaction_id,
)
return updated_payment
if status_normalized not in {'paid', 'paid_over'}:
- logger.info('Heleket платеж %s в статусе %s, зачисление не требуется', updated_payment.uuid, status)
+ logger.info('Heleket платеж в статусе , зачисление не требуется', uuid=updated_payment.uuid, status=status)
return updated_payment
amount_kopeks = updated_payment.amount_kopeks
if amount_kopeks <= 0:
- logger.error('Heleket платеж %s имеет некорректную сумму: %s', updated_payment.uuid, updated_payment.amount)
+ logger.error(
+ 'Heleket платеж имеет некорректную сумму', uuid=updated_payment.uuid, amount=updated_payment.amount
+ )
return None
transaction = await payment_module.create_transaction(
@@ -339,7 +325,7 @@ class HeleketPaymentMixin:
get_user_by_id = payment_module.get_user_by_id
user = await get_user_by_id(db, updated_payment.user_id)
if not user:
- logger.error('Пользователь %s не найден для Heleket платежа', updated_payment.user_id)
+ logger.error('Пользователь не найден для Heleket платежа', user_id=updated_payment.user_id)
return None
old_balance = user.balance_kopeks
@@ -361,7 +347,7 @@ class HeleketPaymentMixin:
getattr(self, 'bot', None),
)
except Exception as error: # pragma: no cover - defensive
- logger.error('Ошибка реферального начисления Heleket: %s', error)
+ logger.error('Ошибка реферального начисления Heleket', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -393,7 +379,7 @@ class HeleketPaymentMixin:
db=db,
)
except Exception as error: # pragma: no cover
- logger.error('Ошибка отправки админ-уведомления Heleket: %s', error)
+ logger.error('Ошибка отправки админ-уведомления Heleket', error=error)
# Отправляем уведомление только Telegram-пользователям
if user.telegram_id:
@@ -426,9 +412,9 @@ class HeleketPaymentMixin:
reply_markup=keyboard,
)
except Exception as error: # pragma: no cover
- logger.error('Ошибка отправки уведомления пользователю Heleket: %s', error)
+ logger.error('Ошибка отправки уведомления пользователю Heleket', error=error)
else:
- logger.info(f'Пропуск Telegram-уведомления Heleket для email-пользователя {user.id}')
+ logger.info('Пропуск Telegram-уведомления Heleket для email-пользователя', user_id=user.id)
# Автопокупка из сохранённой корзины и умная автоактивация
try:
@@ -445,9 +431,9 @@ class HeleketPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -456,10 +442,7 @@ class HeleketPaymentMixin:
except Exception as error:
logger.error(
- 'Ошибка при работе с автоактивацией для пользователя %s: %s',
- user.id,
- error,
- exc_info=True,
+ 'Ошибка при работе с автоактивацией для пользователя', user_id=user.id, error=error, exc_info=True
)
return updated_payment
@@ -491,7 +474,7 @@ class HeleketPaymentMixin:
payment = await heleket_crud.get_heleket_payment_by_id(db, local_payment_id)
if not payment:
- logger.error('Heleket платеж с id=%s не найден', local_payment_id)
+ logger.error('Heleket платеж с id= не найден', local_payment_id=local_payment_id)
return None
payload: dict[str, Any] | None = None
@@ -501,11 +484,7 @@ class HeleketPaymentMixin:
order_id=payment.order_id,
)
except Exception as error: # pragma: no cover - defensive
- logger.exception(
- 'Ошибка получения статуса Heleket платежа %s: %s',
- payment.uuid,
- error,
- )
+ logger.exception('Ошибка получения статуса Heleket платежа', uuid=payment.uuid, error=error)
else:
if response:
result = response.get('result') if isinstance(response, dict) else None
@@ -513,18 +492,13 @@ class HeleketPaymentMixin:
payload = dict(result)
else:
logger.error(
- 'Некорректный ответ Heleket API при проверке платежа %s: %s',
- payment.uuid,
- response,
+ 'Некорректный ответ Heleket API при проверке платежа', uuid=payment.uuid, response=response
)
if payload is None:
fallback = await self._lookup_heleket_payment_history(payment)
if not fallback:
- logger.warning(
- 'Heleket API не вернул информацию по платежу %s',
- payment.uuid,
- )
+ logger.warning('Heleket API не вернул информацию по платежу', uuid=payment.uuid)
return payment
payload = dict(fallback)
diff --git a/app/services/payment/kassa_ai.py b/app/services/payment/kassa_ai.py
index 5f49f111..d256bb92 100644
--- a/app/services/payment/kassa_ai.py
+++ b/app/services/payment/kassa_ai.py
@@ -54,17 +54,17 @@ class KassaAiPaymentMixin:
# Валидация лимитов
if amount_kopeks < settings.KASSA_AI_MIN_AMOUNT_KOPEKS:
logger.warning(
- 'KassaAI: сумма %s меньше минимальной %s',
- amount_kopeks,
- settings.KASSA_AI_MIN_AMOUNT_KOPEKS,
+ 'KassaAI: сумма меньше минимальной',
+ amount_kopeks=amount_kopeks,
+ KASSA_AI_MIN_AMOUNT_KOPEKS=settings.KASSA_AI_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.KASSA_AI_MAX_AMOUNT_KOPEKS:
logger.warning(
- 'KassaAI: сумма %s больше максимальной %s',
- amount_kopeks,
- settings.KASSA_AI_MAX_AMOUNT_KOPEKS,
+ 'KassaAI: сумма больше максимальной',
+ amount_kopeks=amount_kopeks,
+ KASSA_AI_MAX_AMOUNT_KOPEKS=settings.KASSA_AI_MAX_AMOUNT_KOPEKS,
)
return None
@@ -105,11 +105,7 @@ class KassaAiPaymentMixin:
logger.error('KassaAI API не вернул URL платежа')
return None
- logger.info(
- 'KassaAI API: создан заказ order_id=%s, url=%s',
- order_id,
- payment_url,
- )
+ logger.info('KassaAI API: создан заказ order_id url', order_id=order_id, payment_url=payment_url)
# Импортируем CRUD модуль
kassa_ai_crud = import_module('app.database.crud.kassa_ai')
@@ -129,11 +125,11 @@ class KassaAiPaymentMixin:
)
logger.info(
- 'KassaAI: создан платеж order_id=%s, user_id=%s, amount=%s %s',
- order_id,
- user_id,
- amount_rubles,
- currency,
+ 'KassaAI: создан платеж order_id user_id amount',
+ order_id=order_id,
+ user_id=user_id,
+ amount_rubles=amount_rubles,
+ currency=currency,
)
return {
@@ -147,7 +143,7 @@ class KassaAiPaymentMixin:
}
except Exception as e:
- logger.exception('KassaAI: ошибка создания платежа: %s', e)
+ logger.exception('KassaAI: ошибка создания платежа', e=e)
return None
async def process_kassa_ai_webhook(
@@ -179,7 +175,7 @@ class KassaAiPaymentMixin:
try:
# Проверка подписи
if not kassa_ai_service.verify_webhook_signature(merchant_id, amount, order_id, sign):
- logger.warning('KassaAI webhook: неверная подпись для order_id=%s', order_id)
+ logger.warning('KassaAI webhook: неверная подпись для order_id', order_id=order_id)
return False
# Импортируем CRUD модуль
@@ -188,21 +184,21 @@ class KassaAiPaymentMixin:
# Получаем платеж из БД
payment = await kassa_ai_crud.get_kassa_ai_payment_by_order_id(db, order_id)
if not payment:
- logger.warning('KassaAI webhook: платеж не найден order_id=%s', order_id)
+ logger.warning('KassaAI webhook: платеж не найден order_id', order_id=order_id)
return False
# Проверка дублирования
if payment.is_paid:
- logger.info('KassaAI webhook: платеж уже обработан order_id=%s', order_id)
+ logger.info('KassaAI webhook: платеж уже обработан order_id', order_id=order_id)
return True
# Проверка суммы
expected_amount = payment.amount_kopeks / 100
if abs(amount - expected_amount) > 0.01:
logger.warning(
- 'KassaAI webhook: несоответствие суммы ожидалось=%s, получено=%s',
- expected_amount,
- amount,
+ 'KassaAI webhook: несоответствие суммы ожидалось получено',
+ expected_amount=expected_amount,
+ amount=amount,
)
return False
@@ -229,7 +225,7 @@ class KassaAiPaymentMixin:
return await self._finalize_kassa_ai_payment(db, payment, intid=intid, trigger='webhook')
except Exception as e:
- logger.exception('KassaAI webhook: ошибка обработки: %s', e)
+ logger.exception('KassaAI webhook: ошибка обработки', e=e)
return False
async def _finalize_kassa_ai_payment(
@@ -245,9 +241,7 @@ class KassaAiPaymentMixin:
if payment.transaction_id:
logger.info(
- 'KassaAI платеж %s уже привязан к транзакции (trigger=%s)',
- payment.order_id,
- trigger,
+ 'KassaAI платеж уже привязан к транзакции (trigger=)', order_id=payment.order_id, trigger=trigger
)
return True
@@ -255,10 +249,10 @@ class KassaAiPaymentMixin:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
- 'Пользователь %s не найден для KassaAI платежа %s (trigger=%s)',
- payment.user_id,
- payment.order_id,
- trigger,
+ 'Пользователь не найден для KassaAI платежа (trigger=)',
+ user_id=payment.user_id,
+ order_id=payment.order_id,
+ trigger=trigger,
)
return False
@@ -304,7 +298,7 @@ class KassaAiPaymentMixin:
await process_referral_topup(db, user.id, payment.amount_kopeks, getattr(self, 'bot', None))
except Exception as error:
- logger.error('Ошибка обработки реферального пополнения KassaAI: %s', error)
+ logger.error('Ошибка обработки реферального пополнения KassaAI', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -332,7 +326,7 @@ class KassaAiPaymentMixin:
db=db,
)
except Exception as error:
- logger.error('Ошибка отправки админ уведомления KassaAI: %s', error)
+ logger.error('Ошибка отправки админ уведомления KassaAI', error=error)
# Отправка уведомления пользователю (только Telegram-пользователям)
if getattr(self, 'bot', None) and user.telegram_id:
@@ -355,7 +349,7 @@ class KassaAiPaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.error('Ошибка отправки уведомления пользователю KassaAI: %s', error)
+ logger.error('Ошибка отправки уведомления пользователю KassaAI', error=error)
# Автопокупка подписки
try:
@@ -375,9 +369,9 @@ class KassaAiPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -420,17 +414,14 @@ class KassaAiPaymentMixin:
)
except Exception as error:
logger.error(
- 'Ошибка при работе с сохраненной корзиной для пользователя %s: %s',
- user.id,
- error,
- exc_info=True,
+ 'Ошибка при работе с сохраненной корзиной для пользователя', user_id=user.id, error=error, exc_info=True
)
logger.info(
- '✅ Обработан KassaAI платеж %s для пользователя %s (trigger=%s)',
- payment.order_id,
- payment.user_id,
- trigger,
+ '✅ Обработан KassaAI платеж для пользователя (trigger=)',
+ order_id=payment.order_id,
+ user_id=payment.user_id,
+ trigger=trigger,
)
return True
@@ -454,7 +445,7 @@ class KassaAiPaymentMixin:
status_data = await kassa_ai_service.get_order_status(order_id)
return status_data
except Exception as e:
- logger.exception('KassaAI: ошибка проверки статуса: %s', e)
+ logger.exception('KassaAI: ошибка проверки статуса', e=e)
return None
async def get_kassa_ai_payment_status(
@@ -466,12 +457,12 @@ class KassaAiPaymentMixin:
Проверяет статус платежа KassaAI по локальному ID через API.
Если платёж оплачен — автоматически начисляет баланс.
"""
- logger.info('KassaAI: checking payment status for id=%s', local_payment_id)
+ logger.info('KassaAI: checking payment status for id', local_payment_id=local_payment_id)
kassa_ai_crud = import_module('app.database.crud.kassa_ai')
payment = await kassa_ai_crud.get_kassa_ai_payment_by_id(db, local_payment_id)
if not payment:
- logger.warning('KassaAI payment not found: id=%s', local_payment_id)
+ logger.warning('KassaAI payment not found: id', local_payment_id=local_payment_id)
return None
if payment.is_paid:
@@ -508,7 +499,7 @@ class KassaAiPaymentMixin:
kai_status = int(target_order.get('status', 0))
if kai_status == 1:
- logger.info('KassaAI payment %s confirmed via API', payment.order_id)
+ logger.info('KassaAI payment confirmed via API', order_id=payment.order_id)
callback_payload = {
'check_source': 'api',
@@ -537,7 +528,7 @@ class KassaAiPaymentMixin:
trigger='api_check',
)
except Exception as e:
- logger.error('Error checking KassaAI payment status: %s', e)
+ logger.error('Error checking KassaAI payment status', e=e)
return {
'payment': payment,
diff --git a/app/services/payment/mulenpay.py b/app/services/payment/mulenpay.py
index 423bc8ff..a50a91b5 100644
--- a/app/services/payment/mulenpay.py
+++ b/app/services/payment/mulenpay.py
@@ -32,24 +32,24 @@ class MulenPayPaymentMixin:
display_name = settings.get_mulenpay_display_name()
settings.get_mulenpay_display_name_html()
if not getattr(self, 'mulenpay_service', None):
- logger.error('%s сервис не инициализирован', display_name)
+ logger.error('сервис не инициализирован', display_name=display_name)
return None
if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма %s меньше минимальной: %s < %s',
- display_name,
- amount_kopeks,
- settings.MULENPAY_MIN_AMOUNT_KOPEKS,
+ 'Сумма меньше минимальной: <',
+ display_name=display_name,
+ amount_kopeks=amount_kopeks,
+ MULENPAY_MIN_AMOUNT_KOPEKS=settings.MULENPAY_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма %s больше максимальной: %s > %s',
- display_name,
- amount_kopeks,
- settings.MULENPAY_MAX_AMOUNT_KOPEKS,
+ 'Сумма больше максимальной: >',
+ display_name=display_name,
+ amount_kopeks=amount_kopeks,
+ MULENPAY_MAX_AMOUNT_KOPEKS=settings.MULENPAY_MAX_AMOUNT_KOPEKS,
)
return None
@@ -79,7 +79,7 @@ class MulenPayPaymentMixin:
)
if not response:
- logger.error('Ошибка создания %s платежа', display_name)
+ logger.error('Ошибка создания платежа', display_name=display_name)
return None
mulen_payment_id = response.get('id')
@@ -105,11 +105,11 @@ class MulenPayPaymentMixin:
)
logger.info(
- 'Создан %s платеж %s на %s₽ для пользователя %s',
- display_name,
- mulen_payment_id,
- amount_rubles,
- user_id,
+ 'Создан платеж на ₽ для пользователя',
+ display_name=display_name,
+ mulen_payment_id=mulen_payment_id,
+ amount_rubles=amount_rubles,
+ user_id=user_id,
)
return {
@@ -122,7 +122,7 @@ class MulenPayPaymentMixin:
}
except Exception as error:
- logger.error('Ошибка создания %s платежа: %s', display_name, error)
+ logger.error('Ошибка создания платежа', display_name=display_name, error=error)
return None
async def process_mulenpay_callback(
@@ -149,15 +149,15 @@ class MulenPayPaymentMixin:
mulen_payment_id_int = None
amount_value = callback_data.get('amount')
logger.debug(
- '%s callback: uuid=%s, status=%s, amount=%s',
- display_name,
- uuid_value,
- payment_status,
- amount_value,
+ 'callback: uuid status amount',
+ display_name=display_name,
+ uuid_value=uuid_value,
+ payment_status=payment_status,
+ amount_value=amount_value,
)
if not uuid_value and mulen_payment_id_raw is None:
- logger.error('%s callback без uuid и id', display_name)
+ logger.error('callback без uuid и id', display_name=display_name)
return False
payment = None
@@ -169,10 +169,10 @@ class MulenPayPaymentMixin:
if not payment:
logger.error(
- '%s платеж не найден (uuid=%s, id=%s)',
- display_name,
- uuid_value,
- mulen_payment_id_raw,
+ 'платеж не найден (uuid id=)',
+ display_name=display_name,
+ uuid_value=uuid_value,
+ mulen_payment_id_raw=mulen_payment_id_raw,
)
return False
@@ -189,10 +189,10 @@ class MulenPayPaymentMixin:
await self.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - depends on bot rights
logger.warning(
- 'Не удалось удалить %s счёт %s: %s',
- display_name,
- message_id,
- delete_error,
+ 'Не удалось удалить счёт',
+ display_name=display_name,
+ message_id=message_id,
+ delete_error=delete_error,
)
else:
metadata.pop('invoice_message', None)
@@ -208,15 +208,13 @@ class MulenPayPaymentMixin:
)
except Exception as error: # pragma: no cover - diagnostics
logger.warning(
- 'Не удалось обновить метаданные %s после удаления счёта: %s',
- display_name,
- error,
+ 'Не удалось обновить метаданные после удаления счёта',
+ display_name=display_name,
+ error=error,
)
logger.info(
- '%s платеж %s уже обработан, игнорируем повторный callback',
- display_name,
- payment.uuid,
+ 'платеж уже обработан, игнорируем повторный callback', display_name=display_name, uuid=payment.uuid
)
return True
@@ -231,11 +229,7 @@ class MulenPayPaymentMixin:
)
if payment.transaction_id:
- logger.info(
- 'Для %s платежа %s уже создана транзакция',
- display_name,
- payment.uuid,
- )
+ logger.info('Для платежа уже создана транзакция', display_name=display_name, uuid=payment.uuid)
return True
payment_description = getattr(
@@ -265,9 +259,7 @@ class MulenPayPaymentMixin:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
- 'Пользователь %s не найден при обработке %s',
- payment.user_id,
- display_name,
+ 'Пользователь не найден при обработке', user_id=payment.user_id, display_name=display_name
)
return False
@@ -292,11 +284,7 @@ class MulenPayPaymentMixin:
getattr(self, 'bot', None),
)
except Exception as error:
- logger.error(
- 'Ошибка обработки реферального пополнения %s: %s',
- display_name,
- error,
- )
+ logger.error('Ошибка обработки реферального пополнения', display_name=display_name, error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -307,9 +295,9 @@ class MulenPayPaymentMixin:
user = await payment_module.get_user_by_id(db, user.id)
if not user:
logger.error(
- 'Пользователь %s не найден при повторной загрузке после %s',
- payment.user_id,
- display_name,
+ 'Пользователь не найден при повторной загрузке после',
+ user_id=payment.user_id,
+ display_name=display_name,
)
return False
@@ -336,11 +324,7 @@ class MulenPayPaymentMixin:
db=db,
)
except Exception as error:
- logger.error(
- 'Ошибка отправки уведомления о пополнении %s: %s',
- display_name,
- error,
- )
+ logger.error('Ошибка отправки уведомления о пополнении', display_name=display_name, error=error)
if getattr(self, 'bot', None) and user.telegram_id:
try:
@@ -358,11 +342,7 @@ class MulenPayPaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.error(
- 'Ошибка отправки уведомления пользователю %s: %s',
- display_name,
- error,
- )
+ logger.error('Ошибка отправки уведомления пользователю', display_name=display_name, error=error)
# Проверяем наличие сохраненной корзины для возврата к оформлению подписки
try:
@@ -381,9 +361,9 @@ class MulenPayPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -425,19 +405,22 @@ class MulenPayPaymentMixin:
reply_markup=keyboard,
)
logger.info(
- 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю %s',
- user.id,
+ 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю',
+ user_id=user.id,
)
except Exception as e:
logger.error(
- f'Ошибка при работе с сохраненной корзиной для пользователя {user.id}: {e}', exc_info=True
+ 'Ошибка при работе с сохраненной корзиной для пользователя',
+ user_id=user.id,
+ error=e,
+ exc_info=True,
)
logger.info(
- '✅ Обработан %s платеж %s для пользователя %s',
- display_name,
- payment.uuid,
- payment.user_id,
+ '✅ Обработан платеж для пользователя',
+ display_name=display_name,
+ uuid=payment.uuid,
+ user_id=payment.user_id,
)
return True
@@ -449,7 +432,7 @@ class MulenPayPaymentMixin:
callback_payload=callback_data,
mulen_payment_id=mulen_payment_id_int,
)
- logger.info('%s платеж %s отменен', display_name, payment.uuid)
+ logger.info('платеж отменен', display_name=display_name, uuid=payment.uuid)
return True
await payment_module.update_mulenpay_payment_status(
@@ -460,20 +443,15 @@ class MulenPayPaymentMixin:
mulen_payment_id=mulen_payment_id_int,
)
logger.info(
- 'Получен %s callback со статусом %s для платежа %s',
- display_name,
- payment_status,
- payment.uuid,
+ 'Получен callback со статусом для платежа',
+ display_name=display_name,
+ payment_status=payment_status,
+ uuid=payment.uuid,
)
return True
except Exception as error:
- logger.error(
- 'Ошибка обработки %s callback: %s',
- display_name,
- error,
- exc_info=True,
- )
+ logger.error('Ошибка обработки callback', display_name=display_name, error=error, exc_info=True)
return False
def _map_mulenpay_status(self, status_code: int | None) -> str:
@@ -566,10 +544,5 @@ class MulenPayPaymentMixin:
}
except Exception as error:
- logger.error(
- 'Ошибка получения статуса %s: %s',
- display_name,
- error,
- exc_info=True,
- )
+ logger.error('Ошибка получения статуса', display_name=display_name, error=error, exc_info=True)
return None
diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py
index d486950b..7c047d9a 100644
--- a/app/services/payment/pal24.py
+++ b/app/services/payment/pal24.py
@@ -42,17 +42,17 @@ class Pal24PaymentMixin:
if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма Pal24 меньше минимальной: %s < %s',
- amount_kopeks,
- settings.PAL24_MIN_AMOUNT_KOPEKS,
+ 'Сумма Pal24 меньше минимальной: <',
+ amount_kopeks=amount_kopeks,
+ PAL24_MIN_AMOUNT_KOPEKS=settings.PAL24_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма Pal24 больше максимальной: %s > %s',
- amount_kopeks,
- settings.PAL24_MAX_AMOUNT_KOPEKS,
+ 'Сумма Pal24 больше максимальной: >',
+ amount_kopeks=amount_kopeks,
+ PAL24_MAX_AMOUNT_KOPEKS=settings.PAL24_MAX_AMOUNT_KOPEKS,
)
return None
@@ -81,16 +81,16 @@ class Pal24PaymentMixin:
payment_method=api_payment_method,
)
except Pal24APIError as error:
- logger.error('Ошибка Pal24 API при создании счета: %s', error)
+ logger.error('Ошибка Pal24 API при создании счета', error=error)
return None
if not response.get('success', True):
- logger.error('Pal24 вернул ошибку при создании счета: %s', response)
+ logger.error('Pal24 вернул ошибку при создании счета', response=response)
return None
bill_id = response.get('bill_id')
if not bill_id:
- logger.error('Pal24 не вернул bill_id: %s', response)
+ logger.error('Pal24 не вернул bill_id', response=response)
return None
def _pick_url(*keys: str) -> str | None:
@@ -169,10 +169,10 @@ class Pal24PaymentMixin:
)
logger.info(
- 'Создан Pal24 счет %s для пользователя %s (%s₽)',
- bill_id,
- user_id,
- amount_kopeks / 100,
+ 'Создан Pal24 счет для пользователя (₽)',
+ bill_id=bill_id,
+ user_id=user_id,
+ amount_kopeks=amount_kopeks / 100,
)
payment_status = getattr(payment, 'status', response.get('status', 'NEW'))
@@ -230,7 +230,7 @@ class Pal24PaymentMixin:
status = (callback.get('status') or callback.get('Status') or '').upper()
if not bill_id and not order_id:
- logger.error('Pal24 callback без идентификаторов: %s', callback)
+ logger.error('Pal24 callback без идентификаторов', callback=callback)
return False
payment = None
@@ -240,11 +240,11 @@ class Pal24PaymentMixin:
payment = await payment_module.get_pal24_payment_by_order_id(db, order_id)
if not payment:
- logger.error('Pal24 платеж не найден: %s / %s', bill_id, order_id)
+ logger.error('Pal24 платеж не найден: /', bill_id=bill_id, order_id=order_id)
return False
if payment.is_paid:
- logger.info('Pal24 платеж %s уже обработан', payment.bill_id)
+ logger.info('Pal24 платеж уже обработан', bill_id=payment.bill_id)
return True
if status in {'PAID', 'SUCCESS', 'OVERPAID'}:
@@ -300,15 +300,11 @@ class Pal24PaymentMixin:
balance_currency=callback.get('BalanceCurrency') or callback.get('balance_currency'),
payer_account=callback.get('AccountNumber') or callback.get('account') or callback.get('Account'),
)
- logger.info(
- 'Обновили Pal24 платеж %s до статуса %s',
- payment.bill_id,
- status,
- )
+ logger.info('Обновили Pal24 платеж до статуса', bill_id=payment.bill_id, status=status)
return True
except Exception as error:
- logger.error('Ошибка обработки Pal24 callback: %s', error, exc_info=True)
+ logger.error('Ошибка обработки Pal24 callback', error=error, exc_info=True)
return False
async def _finalize_pal24_payment(
@@ -335,9 +331,7 @@ class Pal24PaymentMixin:
await self.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - depends on rights
logger.warning(
- 'Не удалось удалить счёт PayPalych %s: %s',
- message_id,
- delete_error,
+ 'Не удалось удалить счёт PayPalych', message_id=message_id, delete_error=delete_error
)
else:
metadata.pop('invoice_message', None)
@@ -353,26 +347,19 @@ class Pal24PaymentMixin:
)
payment.metadata_json = metadata
except Exception as error: # pragma: no cover - diagnostics
- logger.warning(
- 'Не удалось обновить метаданные PayPalych после удаления счёта: %s',
- error,
- )
+ logger.warning('Не удалось обновить метаданные PayPalych после удаления счёта', error=error)
if payment.transaction_id:
- logger.info(
- 'Pal24 платеж %s уже привязан к транзакции (trigger=%s)',
- payment.bill_id,
- trigger,
- )
+ logger.info('Pal24 платеж уже привязан к транзакции (trigger=)', bill_id=payment.bill_id, trigger=trigger)
return True
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
- 'Пользователь %s не найден для Pal24 платежа %s (trigger=%s)',
- payment.user_id,
- payment.bill_id,
- trigger,
+ 'Пользователь не найден для Pal24 платежа (trigger=)',
+ user_id=payment.user_id,
+ bill_id=payment.bill_id,
+ trigger=trigger,
)
return False
@@ -408,10 +395,7 @@ class Pal24PaymentMixin:
await process_referral_topup(db, user.id, payment.amount_kopeks, getattr(self, 'bot', None))
except Exception as error:
- logger.error(
- 'Ошибка обработки реферального пополнения Pal24: %s',
- error,
- )
+ logger.error('Ошибка обработки реферального пополнения Pal24', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -438,10 +422,7 @@ class Pal24PaymentMixin:
db=db,
)
except Exception as error:
- logger.error(
- 'Ошибка отправки админ уведомления Pal24: %s',
- error,
- )
+ logger.error('Ошибка отправки админ уведомления Pal24', error=error)
if getattr(self, 'bot', None) and user.telegram_id:
try:
@@ -459,10 +440,7 @@ class Pal24PaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.error(
- 'Ошибка отправки уведомления пользователю Pal24: %s',
- error,
- )
+ logger.error('Ошибка отправки уведомления пользователю Pal24', error=error)
try:
from aiogram import types
@@ -480,9 +458,9 @@ class Pal24PaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -531,27 +509,20 @@ class Pal24PaymentMixin:
reply_markup=keyboard,
)
logger.info(
- 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю %s',
- user.id,
+ 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю', user_id=user.id
)
else:
- logger.info(
- 'У пользователя %s нет сохраненной корзины или автопокупка выполнена',
- user.id,
- )
+ logger.info('У пользователя нет сохраненной корзины или автопокупка выполнена', user_id=user.id)
except Exception as error:
logger.error(
- 'Ошибка при работе с сохраненной корзиной для пользователя %s: %s',
- user.id,
- error,
- exc_info=True,
+ 'Ошибка при работе с сохраненной корзиной для пользователя', user_id=user.id, error=error, exc_info=True
)
logger.info(
- '✅ Обработан Pal24 платеж %s для пользователя %s (trigger=%s)',
- payment.bill_id,
- payment.user_id,
- trigger,
+ '✅ Обработан Pal24 платеж для пользователя (trigger=)',
+ bill_id=payment.bill_id,
+ user_id=payment.user_id,
+ trigger=trigger,
)
return True
@@ -579,7 +550,7 @@ class Pal24PaymentMixin:
try:
response = await service.get_bill_status(bill_id_str)
except Pal24APIError as error:
- logger.error('Ошибка Pal24 API при получении статуса счёта: %s', error)
+ logger.error('Ошибка Pal24 API при получении статуса счёта', error=error)
else:
if response:
remote_payloads['bill_status'] = response
@@ -595,7 +566,7 @@ class Pal24PaymentMixin:
try:
payment_response = await service.get_payment_status(payment_id_str)
except Pal24APIError as error:
- logger.error('Ошибка Pal24 API при получении статуса платежа: %s', error)
+ logger.error('Ошибка Pal24 API при получении статуса платежа', error=error)
else:
if payment_response:
remote_payloads['payment_status'] = payment_response
@@ -606,7 +577,7 @@ class Pal24PaymentMixin:
try:
payments_response = await service.get_bill_payments(bill_id_str)
except Pal24APIError as error:
- logger.error('Ошибка Pal24 API при получении списка платежей: %s', error)
+ logger.error('Ошибка Pal24 API при получении списка платежей', error=error)
else:
if payments_response:
remote_payloads['bill_payments'] = payments_response
@@ -711,11 +682,7 @@ class Pal24PaymentMixin:
if finalized:
payment = await payment_module.get_pal24_payment_by_id(db, local_payment_id)
except Exception as error:
- logger.error(
- 'Ошибка автоматического начисления по Pal24 статусу: %s',
- error,
- exc_info=True,
- )
+ logger.error('Ошибка автоматического начисления по Pal24 статусу', error=error, exc_info=True)
links_map, selected_method = self._build_links_map(payment, remote_payloads)
primary_url = (
@@ -740,7 +707,7 @@ class Pal24PaymentMixin:
}
except Exception as error:
- logger.error('Ошибка получения статуса Pal24: %s', error, exc_info=True)
+ logger.error('Ошибка получения статуса Pal24', error=error, exc_info=True)
return None
@staticmethod
diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py
index 4b0477cc..cc870bb4 100644
--- a/app/services/payment/platega.py
+++ b/app/services/payment/platega.py
@@ -43,17 +43,17 @@ class PlategaPaymentMixin:
if amount_kopeks < settings.PLATEGA_MIN_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма Platega меньше минимальной: %s < %s',
- amount_kopeks,
- settings.PLATEGA_MIN_AMOUNT_KOPEKS,
+ 'Сумма Platega меньше минимальной: <',
+ amount_kopeks=amount_kopeks,
+ PLATEGA_MIN_AMOUNT_KOPEKS=settings.PLATEGA_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.PLATEGA_MAX_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма Platega больше максимальной: %s > %s',
- amount_kopeks,
- settings.PLATEGA_MAX_AMOUNT_KOPEKS,
+ 'Сумма Platega больше максимальной: >',
+ amount_kopeks=amount_kopeks,
+ PLATEGA_MAX_AMOUNT_KOPEKS=settings.PLATEGA_MAX_AMOUNT_KOPEKS,
)
return None
@@ -73,7 +73,7 @@ class PlategaPaymentMixin:
payload=payload_token,
)
except Exception as error: # pragma: no cover - network errors
- logger.exception('Ошибка Platega при создании платежа: %s', error)
+ logger.exception('Ошибка Platega при создании платежа', error=error)
return None
if not response:
@@ -112,11 +112,11 @@ class PlategaPaymentMixin:
)
logger.info(
- 'Создан Platega платеж %s для пользователя %s (метод %s, сумма %s₽)',
- transaction_id or payment.id,
- user_id,
- payment_method_code,
- amount_value,
+ 'Создан Platega платеж для пользователя (метод , сумма ₽)',
+ transaction_id=transaction_id or payment.id,
+ user_id=user_id,
+ payment_method_code=payment_method_code,
+ amount_value=amount_value,
)
return {
@@ -148,12 +148,12 @@ class PlategaPaymentMixin:
)
if not payment:
- logger.warning('Platega webhook: платеж не найден (id=%s)', transaction_id)
+ logger.warning('Platega webhook: платеж не найден (id=)', transaction_id=transaction_id)
return False
status_raw = str(payload.get('status') or '').upper()
if not status_raw:
- logger.warning('Platega webhook без статуса для платежа %s', payment.id)
+ logger.warning('Platega webhook без статуса для платежа', payment_id=payment.id)
return False
update_kwargs = {
@@ -166,7 +166,7 @@ class PlategaPaymentMixin:
if status_raw in self._SUCCESS_STATUSES:
if payment.is_paid:
- logger.info('Platega платеж %s уже помечен как оплачен', payment.correlation_id)
+ logger.info('Platega платеж уже помечен как оплачен', correlation_id=payment.correlation_id)
await payment_module.update_platega_payment(
db,
payment=payment,
@@ -190,7 +190,7 @@ class PlategaPaymentMixin:
**update_kwargs,
is_paid=False,
)
- logger.info('Platega платеж %s перешёл в статус %s', payment.correlation_id, status_raw)
+ logger.info('Platega платеж перешёл в статус', correlation_id=payment.correlation_id, status_raw=status_raw)
return True
await payment_module.update_platega_payment(
@@ -219,9 +219,9 @@ class PlategaPaymentMixin:
remote_payload = await service.get_transaction(payment.platega_transaction_id)
except Exception as error: # pragma: no cover - network errors
logger.error(
- 'Ошибка Platega при получении транзакции %s: %s',
- payment.platega_transaction_id,
- error,
+ 'Ошибка Platega при получении транзакции',
+ platega_transaction_id=payment.platega_transaction_id,
+ error=error,
)
if remote_payload:
@@ -300,25 +300,21 @@ class PlategaPaymentMixin:
try:
await self.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning(
- 'Не удалось удалить Platega счёт %s: %s',
- message_id,
- delete_error,
- )
+ logger.warning('Не удалось удалить Platega счёт', message_id=message_id, delete_error=delete_error)
else:
metadata.pop('invoice_message', None)
if payment.transaction_id:
logger.info(
- 'Platega платеж %s уже связан с транзакцией %s',
- payment.correlation_id,
- payment.transaction_id,
+ 'Platega платеж уже связан с транзакцией',
+ correlation_id=payment.correlation_id,
+ transaction_id=payment.transaction_id,
)
return payment
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
- logger.error('Пользователь %s не найден для Platega', payment.user_id)
+ logger.error('Пользователь не найден для Platega', user_id=payment.user_id)
return payment
# Убеждаемся, что промогруппы загружены в асинхронном контексте,
@@ -375,10 +371,7 @@ class PlategaPaymentMixin:
should_credit_balance = created_transaction or not balance_already_credited
if not should_credit_balance:
- logger.info(
- 'Platega платеж %s уже зачислил баланс ранее',
- payment.correlation_id,
- )
+ logger.info('Platega платеж уже зачислил баланс ранее', correlation_id=payment.correlation_id)
return payment
old_balance = user.balance_kopeks
@@ -400,7 +393,7 @@ class PlategaPaymentMixin:
getattr(self, 'bot', None),
)
except Exception as error:
- logger.error('Ошибка обработки реферального пополнения Platega: %s', error)
+ logger.error('Ошибка обработки реферального пополнения Platega', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -423,7 +416,7 @@ class PlategaPaymentMixin:
db=db,
)
except Exception as error:
- logger.error('Ошибка отправки админ уведомления Platega: %s', error)
+ logger.error('Ошибка отправки админ уведомления Platega', error=error)
method_title = settings.get_platega_method_display_title(payment.payment_method_code)
@@ -443,7 +436,7 @@ class PlategaPaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.error('Ошибка отправки уведомления пользователю Platega: %s', error)
+ logger.error('Ошибка отправки уведомления пользователю Platega', error=error)
try:
from aiogram import types
@@ -461,9 +454,9 @@ class PlategaPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -516,9 +509,9 @@ class PlategaPaymentMixin:
)
except Exception as error:
logger.error(
- 'Ошибка при работе с сохраненной корзиной для пользователя %s: %s',
- payment.user_id,
- error,
+ 'Ошибка при работе с сохраненной корзиной для пользователя',
+ user_id=payment.user_id,
+ error=error,
exc_info=True,
)
@@ -536,9 +529,9 @@ class PlategaPaymentMixin:
)
logger.info(
- '✅ Обработан Platega платеж %s для пользователя %s',
- payment.correlation_id,
- payment.user_id,
+ '✅ Обработан Platega платеж для пользователя',
+ correlation_id=payment.correlation_id,
+ user_id=payment.user_id,
)
return payment
diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py
index c4630e39..62c054ca 100644
--- a/app/services/payment/stars.py
+++ b/app/services/payment/stars.py
@@ -68,14 +68,14 @@ class TelegramStarsMixin:
)
logger.info(
- 'Создан Stars invoice на %s звезд (~%s)',
- stars_amount,
- settings.format_price(amount_kopeks),
+ 'Создан Stars invoice на звезд (~)',
+ stars_amount=stars_amount,
+ format_price=settings.format_price(amount_kopeks),
)
return invoice_link
except Exception as error:
- logger.error('Ошибка создания Stars invoice: %s', error)
+ logger.error('Ошибка создания Stars invoice', error=error)
raise
async def process_stars_payment(
@@ -116,10 +116,7 @@ class TelegramStarsMixin:
user = await get_user_by_id(db, user_id)
if not user:
- logger.error(
- 'Пользователь с ID %s не найден при обработке Stars платежа',
- user_id,
- )
+ logger.error('Пользователь с ID не найден при обработке Stars платежа', user_id=user_id)
return False
if simple_payload:
@@ -143,7 +140,7 @@ class TelegramStarsMixin:
)
except Exception as error:
- logger.error('Ошибка обработки Stars платежа: %s', error, exc_info=True)
+ logger.error('Ошибка обработки Stars платежа', error=error, exc_info=True)
return False
@staticmethod
@@ -160,10 +157,7 @@ class TelegramStarsMixin:
tail = payload[len(prefix) :]
parts = tail.split('_', 2)
if len(parts) < 3:
- logger.warning(
- 'Payload Stars simple subscription имеет некорректный формат: %s',
- payload,
- )
+ logger.warning('Payload Stars simple subscription имеет некорректный формат', payload=payload)
return None
user_part, subscription_part, period_part = parts
@@ -171,37 +165,28 @@ class TelegramStarsMixin:
try:
payload_user_id = int(user_part)
except ValueError:
- logger.warning(
- 'Не удалось разобрать user_id в payload Stars simple subscription: %s',
- payload,
- )
+ logger.warning('Не удалось разобрать user_id в payload Stars simple subscription', payload=payload)
return None
if payload_user_id != expected_user_id:
logger.warning(
- 'Получен payload Stars simple subscription с чужим user_id: %s (ожидался %s)',
- payload_user_id,
- expected_user_id,
+ 'Получен payload Stars simple subscription с чужим user_id: (ожидался)',
+ payload_user_id=payload_user_id,
+ expected_user_id=expected_user_id,
)
return None
try:
subscription_id = int(subscription_part)
except ValueError:
- logger.warning(
- 'Не удалось разобрать subscription_id в payload Stars simple subscription: %s',
- payload,
- )
+ logger.warning('Не удалось разобрать subscription_id в payload Stars simple subscription', payload=payload)
return None
period_days: int | None = None
try:
period_days = int(period_part)
except ValueError:
- logger.warning(
- 'Не удалось разобрать период в payload Stars simple subscription: %s',
- payload,
- )
+ logger.warning('Не удалось разобрать период в payload Stars simple subscription', payload=payload)
return _SimpleSubscriptionPayload(
subscription_id=subscription_id,
@@ -238,19 +223,19 @@ class TelegramStarsMixin:
pending_subscription = result.scalar_one_or_none()
except Exception as lookup_error: # pragma: no cover - диагностический лог
logger.error(
- 'Ошибка поиска pending подписки %s для пользователя %s: %s',
- payload_data.subscription_id,
- user.id,
- lookup_error,
+ 'Ошибка поиска pending подписки для пользователя',
+ subscription_id=payload_data.subscription_id,
+ user_id=user.id,
+ lookup_error=lookup_error,
exc_info=True,
)
pending_subscription = None
if not pending_subscription:
logger.error(
- 'Не найдена pending подписка %s для пользователя %s',
- payload_data.subscription_id,
- user.id,
+ 'Не найдена pending подписка для пользователя',
+ subscription_id=payload_data.subscription_id,
+ user_id=user.id,
)
return False
@@ -270,18 +255,12 @@ class TelegramStarsMixin:
)
except Exception as error:
logger.error(
- 'Ошибка активации pending подписки для пользователя %s: %s',
- user.id,
- error,
- exc_info=True,
+ 'Ошибка активации pending подписки для пользователя', user_id=user.id, error=error, exc_info=True
)
return False
if not subscription:
- logger.error(
- 'Не удалось активировать pending подписку пользователя %s',
- user.id,
- )
+ logger.error('Не удалось активировать pending подписку пользователя', user_id=user.id)
return False
try:
@@ -296,9 +275,9 @@ class TelegramStarsMixin:
await db.refresh(subscription)
except Exception as sync_error: # pragma: no cover - диагностический лог
logger.error(
- 'Ошибка синхронизации подписки с RemnaWave для пользователя %s: %s',
- user.id,
- sync_error,
+ 'Ошибка синхронизации подписки с RemnaWave для пользователя',
+ user_id=user.id,
+ sync_error=sync_error,
exc_info=True,
)
@@ -352,15 +331,10 @@ class TelegramStarsMixin:
parse_mode='HTML',
)
logger.info(
- '✅ Пользователь %s получил уведомление об оплате подписки через Stars',
- user.telegram_id,
+ '✅ Пользователь получил уведомление об оплате подписки через Stars', telegram_id=user.telegram_id
)
except Exception as error: # pragma: no cover - диагностический лог
- logger.error(
- 'Ошибка отправки уведомления о подписке через Stars: %s',
- error,
- exc_info=True,
- )
+ logger.error('Ошибка отправки уведомления о подписке через Stars', error=error, exc_info=True)
if getattr(self, 'bot', None):
try:
@@ -377,9 +351,7 @@ class TelegramStarsMixin:
)
except Exception as admin_error: # pragma: no cover - диагностический лог
logger.error(
- 'Ошибка уведомления администраторов о подписке через Stars: %s',
- admin_error,
- exc_info=True,
+ 'Ошибка уведомления администраторов о подписке через Stars', admin_error=admin_error, exc_info=True
)
# Начисляем реферальную комиссию за прямую покупку подписки
@@ -393,16 +365,13 @@ class TelegramStarsMixin:
getattr(self, 'bot', None),
)
except Exception as ref_error:
- logger.error(
- 'Ошибка реферального начисления при покупке подписки через Stars: %s',
- ref_error,
- )
+ logger.error('Ошибка реферального начисления при покупке подписки через Stars', ref_error=ref_error)
logger.info(
- '✅ Обработан Stars платеж как покупка подписки: пользователь %s, %s звезд → %s',
- user.id,
- stars_amount,
- settings.format_price(amount_kopeks),
+ '✅ Обработан Stars платеж как покупка подписки: пользователь , звезд →',
+ user_id=user.id,
+ stars_amount=stars_amount,
+ format_price=settings.format_price(amount_kopeks),
)
return True
@@ -434,8 +403,7 @@ class TelegramStarsMixin:
description_for_referral = f'Пополнение Stars: {settings.format_price(amount_kopeks)} ({stars_amount} ⭐)'
logger.info(
- "🔍 Проверка реферальной логики для описания: '%s'",
- description_for_referral,
+ "🔍 Проверка реферальной логики для описания: ''", description_for_referral=description_for_referral
)
lower_description = description_for_referral.lower()
@@ -446,10 +414,7 @@ class TelegramStarsMixin:
allow_referral = contains_allowed_keywords and not contains_forbidden_keywords
if allow_referral:
- logger.info(
- '🔞 Вызов process_referral_topup для пользователя %s',
- user.id,
- )
+ logger.info('🔞 Вызов process_referral_topup для пользователя', user_id=user.id)
try:
from app.services.referral_service import process_referral_topup
@@ -460,14 +425,10 @@ class TelegramStarsMixin:
getattr(self, 'bot', None),
)
except Exception as error: # pragma: no cover - диагностический лог
- logger.error(
- 'Ошибка обработки реферального пополнения: %s',
- error,
- )
+ logger.error('Ошибка обработки реферального пополнения', error=error)
else:
logger.info(
- "❌ Описание '%s' не подходит для реферальной логики",
- description_for_referral,
+ "❌ Описание '' не подходит для реферальной логики", description_for_referral=description_for_referral
)
if was_first_topup and not user.has_made_first_topup:
@@ -477,11 +438,11 @@ class TelegramStarsMixin:
await db.refresh(user)
logger.info(
- '💰 Баланс пользователя %s изменен: %s → %s (Δ +%s)',
- user.telegram_id,
- old_balance,
- user.balance_kopeks,
- amount_kopeks,
+ '💰 Баланс пользователя изменен: → (Δ +)',
+ telegram_id=user.telegram_id,
+ old_balance=old_balance,
+ balance_kopeks=user.balance_kopeks,
+ amount_kopeks=amount_kopeks,
)
if getattr(self, 'bot', None):
@@ -500,11 +461,7 @@ class TelegramStarsMixin:
db=db,
)
except Exception as error: # pragma: no cover - диагностический лог
- logger.error(
- 'Ошибка отправки уведомления о пополнении Stars: %s',
- error,
- exc_info=True,
- )
+ logger.error('Ошибка отправки уведомления о пополнении Stars', error=error, exc_info=True)
# Проверяем наличие сохраненной корзины для возврата к оформлению подписки
try:
@@ -524,9 +481,9 @@ class TelegramStarsMixin:
)
except Exception as auto_error: # pragma: no cover - диагностический лог
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -573,21 +530,17 @@ class TelegramStarsMixin:
reply_markup=keyboard,
)
logger.info(
- 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю %s',
- user.id,
+ 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю', user_id=user.id
)
except Exception as error: # pragma: no cover - диагностический лог
logger.error(
- 'Ошибка при работе с сохраненной корзиной для пользователя %s: %s',
- user.id,
- error,
- exc_info=True,
+ 'Ошибка при работе с сохраненной корзиной для пользователя', user_id=user.id, error=error, exc_info=True
)
logger.info(
- '✅ Обработан Stars платеж: пользователь %s, %s звезд → %s',
- user.id,
- stars_amount,
- settings.format_price(amount_kopeks),
+ '✅ Обработан Stars платеж: пользователь , звезд →',
+ user_id=user.id,
+ stars_amount=stars_amount,
+ format_price=settings.format_price(amount_kopeks),
)
return True
diff --git a/app/services/payment/tribute.py b/app/services/payment/tribute.py
index f4a4d85f..fdcaade9 100644
--- a/app/services/payment/tribute.py
+++ b/app/services/payment/tribute.py
@@ -36,14 +36,12 @@ class TributePaymentMixin:
payment_url = f'https://tribute.ru/pay?amount={amount_kopeks}&user={user_id}'
logger.info(
- 'Создан Tribute платеж на %s₽ для пользователя %s',
- amount_kopeks / 100,
- user_id,
+ 'Создан Tribute платеж на ₽ для пользователя', amount_kopeks=amount_kopeks / 100, user_id=user_id
)
return payment_url
except Exception as error:
- logger.error('Ошибка создания Tribute платежа: %s', error)
+ logger.error('Ошибка создания Tribute платежа', error=error)
raise
def verify_tribute_webhook(self, data: dict[str, object], signature: str) -> bool:
@@ -62,5 +60,5 @@ class TributePaymentMixin:
return hmac.compare_digest(signature, expected_signature)
except Exception as error:
- logger.error('Ошибка проверки Tribute webhook: %s', error)
+ logger.error('Ошибка проверки Tribute webhook', error=error)
return False
diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py
index 67ce5e95..424e85de 100644
--- a/app/services/payment/wata.py
+++ b/app/services/payment/wata.py
@@ -83,17 +83,17 @@ class WataPaymentMixin:
if amount_kopeks < settings.WATA_MIN_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма WATA меньше минимальной: %s < %s',
- amount_kopeks,
- settings.WATA_MIN_AMOUNT_KOPEKS,
+ 'Сумма WATA меньше минимальной: <',
+ amount_kopeks=amount_kopeks,
+ WATA_MIN_AMOUNT_KOPEKS=settings.WATA_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.WATA_MAX_AMOUNT_KOPEKS:
logger.warning(
- 'Сумма WATA больше максимальной: %s > %s',
- amount_kopeks,
- settings.WATA_MAX_AMOUNT_KOPEKS,
+ 'Сумма WATA больше максимальной: >',
+ amount_kopeks=amount_kopeks,
+ WATA_MAX_AMOUNT_KOPEKS=settings.WATA_MAX_AMOUNT_KOPEKS,
)
return None
@@ -108,7 +108,7 @@ class WataPaymentMixin:
elif user.email:
description = f'{description} | {user.email}'
except Exception as error:
- logger.debug('Не удалось получить данные пользователя для описания WATA: %s', error)
+ logger.debug('Не удалось получить данные пользователя для описания WATA', error=error)
order_id = f'wata_{user_id}_{uuid.uuid4().hex[:12]}'
@@ -120,10 +120,10 @@ class WataPaymentMixin:
order_id=order_id,
)
except WataAPIError as error:
- logger.error('Ошибка создания WATA платежа: %s', error)
+ logger.error('Ошибка создания WATA платежа', error=error)
return None
except Exception as error: # pragma: no cover - safety net
- logger.exception('Непредвиденная ошибка при создании WATA платежа: %s', error)
+ logger.exception('Непредвиденная ошибка при создании WATA платежа', error=error)
return None
payment_link_id = response.get('id')
@@ -134,7 +134,7 @@ class WataPaymentMixin:
fail_url = response.get('failRedirectUrl')
if not payment_link_id:
- logger.error('WATA API не вернула идентификатор платежной ссылки: %s', response)
+ logger.error('WATA API не вернула идентификатор платежной ссылки', response=response)
return None
expiration_raw = response.get('expirationDateTime')
@@ -164,10 +164,10 @@ class WataPaymentMixin:
)
logger.info(
- 'Создан WATA платеж %s на %s₽ для пользователя %s',
- payment_link_id,
- amount_kopeks / 100,
- user_id,
+ 'Создан WATA платеж на ₽ для пользователя',
+ payment_link_id=payment_link_id,
+ amount_kopeks=amount_kopeks / 100,
+ user_id=user_id,
)
return {
@@ -188,7 +188,7 @@ class WataPaymentMixin:
payment_module = import_module('app.services.payment_service')
if not isinstance(payload, dict):
- logger.error('WATA webhook payload не является словарём: %s', payload)
+ logger.error('WATA webhook payload не является словарём', payload=payload)
return False
order_id_raw = payload.get('orderId')
@@ -200,14 +200,11 @@ class WataPaymentMixin:
transaction_status = (transaction_status_raw or '').strip()
if not order_id and not payment_link_id:
- logger.error(
- 'WATA webhook без orderId и paymentLinkId: %s',
- payload,
- )
+ logger.error('WATA webhook без orderId и paymentLinkId', payload=payload)
return False
if not transaction_status:
- logger.error('WATA webhook без статуса транзакции: %s', payload)
+ logger.error('WATA webhook без статуса транзакции', payload=payload)
return False
payment = None
@@ -218,9 +215,7 @@ class WataPaymentMixin:
if not payment:
logger.error(
- 'WATA платеж не найден (order_id=%s, payment_link_id=%s)',
- order_id,
- payment_link_id,
+ 'WATA платеж не найден (order_id payment_link_id=)', order_id=order_id, payment_link_id=payment_link_id
)
return False
@@ -252,20 +247,14 @@ class WataPaymentMixin:
if status_lower == 'paid':
if payment.is_paid:
- logger.info(
- 'WATA платеж %s уже помечен как оплачен',
- payment.payment_link_id,
- )
+ logger.info('WATA платеж уже помечен как оплачен', payment_link_id=payment.payment_link_id)
return True
await self._finalize_wata_payment(db, payment, payload)
return True
if status_lower == 'declined':
- logger.info(
- 'WATA платеж %s отклонён',
- payment.payment_link_id,
- )
+ logger.info('WATA платеж отклонён', payment_link_id=payment.payment_link_id)
return True
@@ -288,9 +277,9 @@ class WataPaymentMixin:
try:
remote_link = await self.wata_service.get_payment_link(payment.payment_link_id) # type: ignore[union-attr]
except WataAPIError as error:
- logger.error('Ошибка получения WATA ссылки %s: %s', payment.payment_link_id, error)
+ logger.error('Ошибка получения WATA ссылки', payment_link_id=payment.payment_link_id, error=error)
except Exception as error: # pragma: no cover - safety net
- logger.exception('Непредвиденная ошибка при запросе WATA ссылки: %s', error)
+ logger.exception('Непредвиденная ошибка при запросе WATA ссылки', error=error)
if remote_link:
remote_status = remote_link.get('status') or payment.status
@@ -317,16 +306,12 @@ class WataPaymentMixin:
transaction_id
)
except WataAPIError as error:
- logger.error(
- 'Ошибка получения WATA транзакции %s: %s',
- transaction_id,
- error,
- )
+ logger.error('Ошибка получения WATA транзакции', transaction_id=transaction_id, error=error)
except Exception as error: # pragma: no cover - safety net
logger.exception(
- 'Непредвиденная ошибка при запросе WATA транзакции %s: %s',
- transaction_id,
- error,
+ 'Непредвиденная ошибка при запросе WATA транзакции',
+ transaction_id=transaction_id,
+ error=error,
)
if not transaction_payload:
try:
@@ -343,12 +328,10 @@ class WataPaymentMixin:
break
except WataAPIError as error:
logger.error(
- 'Ошибка поиска WATA транзакций для %s: %s',
- payment.payment_link_id,
- error,
+ 'Ошибка поиска WATA транзакций для', payment_link_id=payment.payment_link_id, error=error
)
except Exception as error: # pragma: no cover - safety net
- logger.exception('Непредвиденная ошибка при поиске WATA транзакции: %s', error)
+ logger.exception('Непредвиденная ошибка при поиске WATA транзакции', error=error)
if not transaction_payload and not payment.is_paid and getattr(self, 'wata_service', None):
fallback_transaction_id = transaction_id or _extract_transaction_id(payment)
@@ -359,15 +342,15 @@ class WataPaymentMixin:
)
except WataAPIError as error:
logger.error(
- 'Ошибка повторного запроса WATA транзакции %s: %s',
- fallback_transaction_id,
- error,
+ 'Ошибка повторного запроса WATA транзакции',
+ fallback_transaction_id=fallback_transaction_id,
+ error=error,
)
except Exception as error: # pragma: no cover - safety net
logger.exception(
- 'Непредвиденная ошибка при повторном запросе WATA транзакции %s: %s',
- fallback_transaction_id,
- error,
+ 'Непредвиденная ошибка при повторном запросе WATA транзакции',
+ fallback_transaction_id=fallback_transaction_id,
+ error=error,
)
if transaction_payload and not payment.is_paid:
@@ -380,9 +363,9 @@ class WataPaymentMixin:
payment = await self._finalize_wata_payment(db, payment, transaction_payload)
else:
logger.debug(
- 'WATA транзакция %s в статусе %s, повторная обработка не требуется',
- transaction_id or getattr(payment, 'payment_link_id', ''),
- normalized_status or 'unknown',
+ 'WATA транзакция в статусе , повторная обработка не требуется',
+ transaction_id=transaction_id or getattr(payment, 'payment_link_id', ''),
+ normalized_status=normalized_status or 'unknown',
)
return {
@@ -407,9 +390,9 @@ class WataPaymentMixin:
paid_status = None
if paid_status and str(paid_status).lower() not in {'paid', 'declined', 'pending'}:
logger.debug(
- 'Неизвестный статус WATA транзакции %s: %s',
- getattr(payment, 'payment_link_id', ''),
- paid_status,
+ 'Неизвестный статус WATA транзакции',
+ getattr=getattr(payment, 'payment_link_id', ''),
+ paid_status=paid_status,
)
paid_at = None
@@ -427,11 +410,7 @@ class WataPaymentMixin:
try:
await self.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - depends on rights
- logger.warning(
- 'Не удалось удалить счёт WATA %s: %s',
- message_id,
- delete_error,
- )
+ logger.warning('Не удалось удалить счёт WATA', message_id=message_id, delete_error=delete_error)
else:
existing_metadata.pop('invoice_message', None)
@@ -449,15 +428,15 @@ class WataPaymentMixin:
if payment.transaction_id:
logger.info(
- 'WATA платеж %s уже привязан к транзакции %s',
- payment.payment_link_id,
- payment.transaction_id,
+ 'WATA платеж уже привязан к транзакции',
+ payment_link_id=payment.payment_link_id,
+ transaction_id=payment.transaction_id,
)
return payment
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
- logger.error('Пользователь %s не найден при обработке WATA', payment.user_id)
+ logger.error('Пользователь не найден при обработке WATA', user_id=payment.user_id)
return payment
transaction_external_id = str(transaction_payload.get('id') or transaction_payload.get('transactionId') or '')
@@ -485,7 +464,7 @@ class WataPaymentMixin:
await db.commit()
user = await payment_module.get_user_by_id(db, user.id)
if not user:
- logger.error('Пользователь %s не найден после коммита WATA', payment.user_id)
+ logger.error('Пользователь не найден после коммита WATA', user_id=payment.user_id)
return payment
promo_group = user.get_primary_promo_group()
@@ -503,7 +482,7 @@ class WataPaymentMixin:
getattr(self, 'bot', None),
)
except Exception as error:
- logger.error('Ошибка обработки реферального пополнения WATA: %s', error)
+ logger.error('Ошибка обработки реферального пополнения WATA', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
@@ -526,7 +505,7 @@ class WataPaymentMixin:
db=db,
)
except Exception as error:
- logger.error('Ошибка отправки админ уведомления WATA: %s', error)
+ logger.error('Ошибка отправки админ уведомления WATA', error=error)
if getattr(self, 'bot', None) and user.telegram_id:
try:
@@ -548,7 +527,7 @@ class WataPaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.error('Ошибка отправки уведомления пользователю WATA: %s', error)
+ logger.error('Ошибка отправки уведомления пользователю WATA', error=error)
try:
from aiogram import types
@@ -566,9 +545,9 @@ class WataPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -613,6 +592,6 @@ class WataPaymentMixin:
reply_markup=keyboard,
)
except Exception as error:
- logger.debug('Не удалось отправить напоминание о корзине после WATA: %s', error)
+ logger.debug('Не удалось отправить напоминание о корзине после WATA', error=error)
return payment
diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py
index ed05d603..1eb87683 100644
--- a/app/services/payment/yookassa.py
+++ b/app/services/payment/yookassa.py
@@ -121,7 +121,7 @@ class YooKassaPaymentMixin:
payment_metadata['user_telegram_id'] = str(user.telegram_id)
payment_metadata['user_username'] = user.username or ''
except Exception as e:
- logger.warning(f'Не удалось получить telegram_id для user_id={user_id}: {e}')
+ logger.warning('Не удалось получить telegram_id для user_id', user_id=user_id, error=e)
# Preserve existing type from metadata if passed (e.g., "trial")
existing_type = payment_metadata.get('type')
@@ -143,7 +143,7 @@ class YooKassaPaymentMixin:
)
if not yookassa_response or yookassa_response.get('error'):
- logger.error('Ошибка создания платежа YooKassa: %s', yookassa_response)
+ logger.error('Ошибка создания платежа YooKassa', yookassa_response=yookassa_response)
return None
yookassa_created_at: datetime | None = None
@@ -152,7 +152,7 @@ class YooKassaPaymentMixin:
dt_with_tz = datetime.fromisoformat(yookassa_response['created_at'].replace('Z', '+00:00'))
yookassa_created_at = dt_with_tz.replace(tzinfo=None)
except Exception as error:
- logger.warning('Не удалось распарсить created_at: %s', error)
+ logger.warning('Не удалось распарсить created_at', error=error)
yookassa_created_at = None
local_payment = await payment_module.create_yookassa_payment(
@@ -171,10 +171,10 @@ class YooKassaPaymentMixin:
)
logger.info(
- 'Создан платеж YooKassa %s на %s₽ для пользователя %s',
- yookassa_response['id'],
- amount_rubles,
- user_id,
+ 'Создан платеж YooKassa на ₽ для пользователя',
+ yookassa_response=yookassa_response['id'],
+ amount_rubles=amount_rubles,
+ user_id=user_id,
)
return {
@@ -188,7 +188,7 @@ class YooKassaPaymentMixin:
}
except Exception as error:
- logger.error('Ошибка создания платежа YooKassa: %s', error)
+ logger.error('Ошибка создания платежа YooKassa', error=error)
return None
async def create_yookassa_sbp_payment(
@@ -223,7 +223,7 @@ class YooKassaPaymentMixin:
payment_metadata['user_telegram_id'] = str(user.telegram_id)
payment_metadata['user_username'] = user.username or ''
except Exception as e:
- logger.warning(f'Не удалось получить telegram_id для user_id={user_id}: {e}')
+ logger.warning('Не удалось получить telegram_id для user_id', user_id=user_id, error=e)
# Preserve existing type from metadata if passed (e.g., "trial")
existing_type = payment_metadata.get('type')
@@ -245,10 +245,7 @@ class YooKassaPaymentMixin:
)
if not yookassa_response or yookassa_response.get('error'):
- logger.error(
- 'Ошибка создания платежа YooKassa СБП: %s',
- yookassa_response,
- )
+ logger.error('Ошибка создания платежа YooKassa СБП', yookassa_response=yookassa_response)
return None
local_payment = await payment_module.create_yookassa_payment(
@@ -267,10 +264,10 @@ class YooKassaPaymentMixin:
)
logger.info(
- 'Создан платеж YooKassa СБП %s на %s₽ для пользователя %s',
- yookassa_response['id'],
- amount_rubles,
- user_id,
+ 'Создан платеж YooKassa СБП на ₽ для пользователя',
+ yookassa_response=yookassa_response['id'],
+ amount_rubles=amount_rubles,
+ user_id=user_id,
)
confirmation_token = (yookassa_response.get('confirmation', {}) or {}).get('confirmation_token')
@@ -288,7 +285,7 @@ class YooKassaPaymentMixin:
}
except Exception as error:
- logger.error('Ошибка создания платежа YooKassa СБП: %s', error)
+ logger.error('Ошибка создания платежа YooKassa СБП', error=error)
return None
async def get_yookassa_payment_status(
@@ -313,9 +310,7 @@ class YooKassaPaymentMixin:
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- 'Ошибка получения статуса YooKassa %s: %s',
- payment.yookassa_payment_id,
- error,
+ 'Ошибка получения статуса YooKassa', yookassa_payment_id=payment.yookassa_payment_id, error=error
)
if remote_data:
@@ -328,9 +323,7 @@ class YooKassaPaymentMixin:
captured_at = datetime.fromisoformat(str(captured_raw).replace('Z', '+00:00')).replace(tzinfo=None)
except Exception as parse_error: # pragma: no cover - diagnostic log
logger.debug(
- 'Не удалось распарсить captured_at %s: %s',
- captured_raw,
- parse_error,
+ 'Не удалось распарсить captured_at', captured_raw=captured_raw, parse_error=parse_error
)
captured_at = None
@@ -358,26 +351,26 @@ class YooKassaPaymentMixin:
transaction_id = getattr(payment, 'transaction_id', None)
except Exception as refresh_error: # pragma: no cover - defensive logging
logger.warning(
- 'Не удалось обновить состояние платежа YooKassa %s перед повторной обработкой: %s',
- payment.yookassa_payment_id,
- refresh_error,
+ 'Не удалось обновить состояние платежа YooKassa перед повторной обработкой',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ refresh_error=refresh_error,
exc_info=True,
)
if transaction_id:
logger.info(
- 'Пропускаем повторную обработку платежа YooKassa %s: уже связан с транзакцией %s',
- payment.yookassa_payment_id,
- transaction_id,
+ 'Пропускаем повторную обработку платежа YooKassa : уже связан с транзакцией',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ transaction_id=transaction_id,
)
else:
try:
await self._process_successful_yookassa_payment(db, payment)
except Exception as process_error: # pragma: no cover - defensive logging
logger.error(
- 'Ошибка обработки успешного платежа YooKassa %s: %s',
- payment.yookassa_payment_id,
- process_error,
+ 'Ошибка обработки успешного платежа YooKassa',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ process_error=process_error,
exc_info=True,
)
@@ -406,9 +399,9 @@ class YooKassaPaymentMixin:
# Fast-path: already processed
if getattr(payment, 'transaction_id', None):
logger.info(
- 'Платеж YooKassa %s уже обработан (transaction_id=%s), пропускаем.',
- payment.yookassa_payment_id,
- payment.transaction_id,
+ 'Платеж YooKassa уже обработан (transaction_id=), пропускаем.',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ transaction_id=payment.transaction_id,
)
return True
@@ -430,9 +423,9 @@ class YooKassaPaymentMixin:
if existing_transaction:
# Если транзакция уже существует, просто завершаем обработку
logger.info(
- 'Платеж YooKassa %s уже был обработан транзакцией %s. Пропускаем повторную обработку.',
- payment.yookassa_payment_id,
- existing_transaction.id,
+ 'Платеж YooKassa уже был обработан транзакцией . Пропускаем повторную обработку.',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ existing_transaction_id=existing_transaction.id,
)
# Убедимся, что платеж связан с транзакцией
@@ -453,10 +446,10 @@ class YooKassaPaymentMixin:
payment.transaction = linked_payment.transaction
except Exception as link_error: # pragma: no cover - защитный лог
logger.warning(
- 'Не удалось привязать платеж YooKassa %s к существующей транзакции %s: %s',
- payment.yookassa_payment_id,
- existing_transaction.id,
- link_error,
+ 'Не удалось привязать платеж YooKassa к существующей транзакции',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ existing_transaction_id=existing_transaction.id,
+ link_error=link_error,
exc_info=True,
)
@@ -471,9 +464,9 @@ class YooKassaPaymentMixin:
payment_metadata = json.loads(payment.metadata_json)
elif isinstance(payment.metadata_json, dict):
payment_metadata = payment.metadata_json
- logger.info(f'Метаданные платежа: {payment_metadata}')
+ logger.info('Метаданные платежа', payment_metadata=payment_metadata)
except Exception as parse_error:
- logger.error(f'Ошибка парсинга метаданных платежа: {parse_error}')
+ logger.error('Ошибка парсинга метаданных платежа', parse_error=parse_error)
invoice_message = payment_metadata.get('invoice_message') or {}
if getattr(self, 'bot', None):
@@ -484,9 +477,7 @@ class YooKassaPaymentMixin:
await self.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - depends on bot rights
logger.warning(
- 'Не удалось удалить сообщение YooKassa %s: %s',
- message_id,
- delete_error,
+ 'Не удалось удалить сообщение YooKassa', message_id=message_id, delete_error=delete_error
)
else:
payment_metadata.pop('invoice_message', None)
@@ -503,26 +494,26 @@ class YooKassaPaymentMixin:
transaction = await get_transaction_by_id(db, existing_transaction_id)
except Exception as fetch_error: # pragma: no cover - диагностический лог
logger.warning(
- 'Не удалось получить транзакцию %s для платежа YooKassa %s: %s',
- existing_transaction_id,
- payment.yookassa_payment_id,
- fetch_error,
+ 'Не удалось получить транзакцию для платежа YooKassa',
+ existing_transaction_id=existing_transaction_id,
+ yookassa_payment_id=payment.yookassa_payment_id,
+ fetch_error=fetch_error,
exc_info=True,
)
if transaction and processing_completed:
logger.info(
- 'Пропускаем повторную обработку платежа YooKassa %s: транзакция %s уже завершила начисление.',
- payment.yookassa_payment_id,
- existing_transaction_id,
+ 'Пропускаем повторную обработку платежа YooKassa : транзакция уже завершила начисление.',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ existing_transaction_id=existing_transaction_id,
)
return True
if transaction:
logger.info(
- 'Транзакция %s для платежа YooKassa %s найдена, но обработка ранее не была завершена — повторяем критические шаги.',
- existing_transaction_id,
- payment.yookassa_payment_id,
+ 'Транзакция для платежа YooKassa найдена, но обработка ранее не была завершена — повторяем критические шаги.',
+ existing_transaction_id=existing_transaction_id,
+ yookassa_payment_id=payment.yookassa_payment_id,
)
if transaction is None:
@@ -542,9 +533,9 @@ class YooKassaPaymentMixin:
if existing_transaction:
# Если транзакция уже существует, пропускаем обработку
logger.info(
- 'Платеж YooKassa %s уже был обработан транзакцией %s. Пропускаем повторную обработку.',
- payment.yookassa_payment_id,
- existing_transaction.id,
+ 'Платеж YooKassa уже был обработан транзакцией . Пропускаем повторную обработку.',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ existing_transaction_id=existing_transaction.id,
)
# Убедимся, что платеж связан с транзакцией
@@ -565,10 +556,10 @@ class YooKassaPaymentMixin:
payment.transaction = linked_payment.transaction
except Exception as link_error: # pragma: no cover - защитный лог
logger.warning(
- 'Не удалось привязать платеж YooKassa %s к существующей транзакции %s: %s',
- payment.yookassa_payment_id,
- existing_transaction.id,
- link_error,
+ 'Не удалось привязать платеж YooKassa к существующей транзакции',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ existing_transaction_id=existing_transaction.id,
+ link_error=link_error,
exc_info=True,
)
@@ -627,9 +618,9 @@ class YooKassaPaymentMixin:
if is_trial_payment:
# Обработка платного триала
logger.info(
- 'YooKassa платеж %s обработан как оплата триала. Баланс пользователя %s не изменяется.',
- payment.yookassa_payment_id,
- user.id,
+ 'YooKassa платеж обработан как оплата триала. Баланс пользователя не изменяется.',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ user_id=user.id,
)
try:
subscription_id = payment_metadata.get('subscription_id')
@@ -646,7 +637,9 @@ class YooKassaPaymentMixin:
if subscription:
logger.info(
- f'Триальная подписка {subscription_id} активирована для пользователя {user.id}'
+ 'Триальная подписка активирована для пользователя',
+ subscription_id=subscription_id,
+ user_id=user.id,
)
# Создаем пользователя в RemnaWave
@@ -654,7 +647,7 @@ class YooKassaPaymentMixin:
try:
await subscription_service.create_remnawave_user(db, subscription)
except Exception as rw_error:
- logger.error(f'Ошибка создания RemnaWave для триала: {rw_error}')
+ logger.error('Ошибка создания RemnaWave для триала', rw_error=rw_error)
# Уведомление админам
if getattr(self, 'bot', None):
@@ -667,7 +660,7 @@ class YooKassaPaymentMixin:
payment_method='YooKassa',
)
except Exception as admin_error:
- logger.warning(f'Ошибка уведомления админов о триале: {admin_error}')
+ logger.warning('Ошибка уведомления админов о триале', admin_error=admin_error)
# Уведомление пользователю (только для Telegram-пользователей)
if getattr(self, 'bot', None) and user.telegram_id:
@@ -684,23 +677,31 @@ class YooKassaPaymentMixin:
parse_mode='HTML',
)
except Exception as notify_error:
- logger.warning(f'Ошибка уведомления пользователя о триале: {notify_error}')
+ logger.warning(
+ 'Ошибка уведомления пользователя о триале', notify_error=notify_error
+ )
elif not user.telegram_id:
logger.info(
- f'Пропуск Telegram-уведомления о триале для email-пользователя {user.id}'
+ 'Пропуск Telegram-уведомления о триале для email-пользователя', user_id=user.id
)
else:
- logger.error(f'Не удалось активировать триал {subscription_id} для {user.id}')
+ logger.error(
+ 'Не удалось активировать триал для',
+ subscription_id=subscription_id,
+ user_id=user.id,
+ )
else:
logger.error('Отсутствует subscription_id в metadata триального платежа YooKassa')
except Exception as trial_error:
- logger.error(f'Ошибка обработки триального платежа YooKassa: {trial_error}', exc_info=True)
+ logger.error(
+ 'Ошибка обработки триального платежа YooKassa', trial_error=trial_error, exc_info=True
+ )
elif is_simple_subscription:
logger.info(
- 'YooKassa платеж %s обработан как покупка подписки. Баланс пользователя %s не изменяется.',
- payment.yookassa_payment_id,
- user.id,
+ 'YooKassa платеж обработан как покупка подписки. Баланс пользователя не изменяется.',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ user_id=user.id,
)
# Начисляем реферальную комиссию за прямую покупку подписки
@@ -715,8 +716,7 @@ class YooKassaPaymentMixin:
)
except Exception as ref_error:
logger.error(
- 'Ошибка реферального начисления при покупке подписки YooKassa: %s',
- ref_error,
+ 'Ошибка реферального начисления при покупке подписки YooKassa', ref_error=ref_error
)
else:
old_balance = getattr(user, 'balance_kopeks', 0)
@@ -772,10 +772,7 @@ class YooKassaPaymentMixin:
getattr(self, 'bot', None),
)
except Exception as error:
- logger.error(
- 'Ошибка обработки реферального пополнения YooKassa: %s',
- error,
- )
+ logger.error('Ошибка обработки реферального пополнения YooKassa', error=error)
if was_first_topup and not getattr(user, 'has_made_first_topup', False):
user.has_made_first_topup = True
@@ -806,9 +803,7 @@ class YooKassaPaymentMixin:
logger.info('Уведомление админам о пополнении отправлено успешно')
except Exception as error:
logger.error(
- 'Ошибка отправки уведомления админам о YooKassa пополнении: %s',
- error,
- exc_info=True, # Добавляем полный стек вызовов для отладки
+ 'Ошибка отправки уведомления админам о YooKassa пополнении', error=error, exc_info=True
)
# Отправляем уведомление пользователю (только Telegram-пользователям)
@@ -824,23 +819,19 @@ class YooKassaPaymentMixin:
)
logger.info('Уведомление пользователю о платеже отправлено успешно')
except Exception as error:
- logger.error(
- 'Ошибка отправки уведомления о платеже: %s',
- error,
- exc_info=True, # Добавляем полный стек вызовов для отладки
- )
+ logger.error('Ошибка отправки уведомления о платеже', error=error, exc_info=True)
# Проверяем наличие сохраненной корзины для возврата к оформлению подписки
# ВАЖНО: этот код должен выполняться даже при ошибках в уведомлениях
- logger.info(f'Проверяем наличие сохраненной корзины для пользователя {user.id}')
+ logger.info('Проверяем наличие сохраненной корзины для пользователя', user_id=user.id)
from app.services.user_cart_service import user_cart_service
try:
has_saved_cart = await user_cart_service.has_user_cart(user.id)
logger.info(
- 'Результат проверки корзины для пользователя %s: %s',
- user.id,
- has_saved_cart,
+ 'Результат проверки корзины для пользователя',
+ user_id=user.id,
+ has_saved_cart=has_saved_cart,
)
auto_purchase_success = False
@@ -853,9 +844,9 @@ class YooKassaPaymentMixin:
)
except Exception as auto_error:
logger.error(
- 'Ошибка автоматической покупки подписки для пользователя %s: %s',
- user.id,
- auto_error,
+ 'Ошибка автоматической покупки подписки для пользователя',
+ user_id=user.id,
+ auto_error=auto_error,
exc_info=True,
)
@@ -908,21 +899,24 @@ class YooKassaPaymentMixin:
reply_markup=keyboard,
)
logger.info(
- f'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю {user.id}'
+ 'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю',
+ user_id=user.id,
)
else:
logger.info(
- 'У пользователя %s нет сохраненной корзины, бот недоступен или покупка уже выполнена',
- user.id,
+ 'У пользователя нет сохраненной корзины, бот недоступен или покупка уже выполнена',
+ user_id=user.id,
)
except Exception as e:
logger.error(
- f'Критическая ошибка при работе с сохраненной корзиной для пользователя {user.id}: {e}',
+ 'Критическая ошибка при работе с сохраненной корзиной для пользователя',
+ user_id=user.id,
+ error=e,
exc_info=True,
)
if is_simple_subscription:
- logger.info(f'Обнаружен платеж простой покупки подписки для пользователя {user.id}')
+ logger.info('Обнаружен платеж простой покупки подписки для пользователя', user_id=user.id)
try:
# Активируем подписку
from app.services.subscription_service import SubscriptionService
@@ -933,7 +927,11 @@ class YooKassaPaymentMixin:
subscription_period = int(payment_metadata.get('subscription_period', 30))
order_id = payment_metadata.get('order_id')
- logger.info(f'Активация подписки: период={subscription_period} дней, заказ={order_id}')
+ logger.info(
+ 'Активация подписки: период= дней, заказ',
+ subscription_period=subscription_period,
+ order_id=order_id,
+ )
# Активируем pending подписку пользователя
from app.database.crud.subscription import activate_pending_subscription
@@ -943,7 +941,7 @@ class YooKassaPaymentMixin:
)
if subscription:
- logger.info(f'Подписка успешно активирована для пользователя {user.id}')
+ logger.info('Подписка успешно активирована для пользователя', user_id=user.id)
# Обновляем данные подписки в RemnaWave, чтобы получить актуальные ссылки
try:
@@ -952,9 +950,9 @@ class YooKassaPaymentMixin:
await db.refresh(subscription)
except Exception as sync_error:
logger.error(
- 'Ошибка синхронизации подписки с RemnaWave для пользователя %s: %s',
- user.id,
- sync_error,
+ 'Ошибка синхронизации подписки с RemnaWave для пользователя',
+ user_id=user.id,
+ sync_error=sync_error,
exc_info=True,
)
@@ -997,7 +995,9 @@ class YooKassaPaymentMixin:
parse_mode='HTML',
)
elif not user.telegram_id:
- logger.info(f'Пропуск Telegram-уведомления о подписке для email-пользователя {user.id}')
+ logger.info(
+ 'Пропуск Telegram-уведомления о подписке для email-пользователя', user_id=user.id
+ )
if getattr(self, 'bot', None):
try:
@@ -1038,14 +1038,16 @@ class YooKassaPaymentMixin:
)
except Exception as admin_error:
logger.error(
- 'Ошибка отправки уведомления админам о покупке подписки через YooKassa: %s',
- admin_error,
+ 'Ошибка отправки уведомления админам о покупке подписки через YooKassa',
+ admin_error=admin_error,
exc_info=True,
)
else:
- logger.error(f'Ошибка активации подписки для пользователя {user.id}')
+ logger.error('Ошибка активации подписки для пользователя', user_id=user.id)
except Exception as e:
- logger.error(f'Ошибка активации подписки для пользователя {user.id}: {e}', exc_info=True)
+ logger.error(
+ 'Ошибка активации подписки для пользователя', user_id=user.id, error=e, exc_info=True
+ )
if not processing_marked:
payment_metadata = await self._mark_yookassa_payment_processing_completed(
@@ -1059,9 +1061,9 @@ class YooKassaPaymentMixin:
critical_flow_completed = True
else:
logger.warning(
- 'Пользователь %s для платежа YooKassa %s не найден — начисление баланса невозможно',
- payment.user_id,
- payment.yookassa_payment_id,
+ 'Пользователь для платежа YooKassa не найден — начисление баланса невозможно',
+ user_id=payment.user_id,
+ yookassa_payment_id=payment.yookassa_payment_id,
)
if critical_flow_completed and not processing_marked:
@@ -1074,17 +1076,17 @@ class YooKassaPaymentMixin:
if is_simple_subscription:
logger.info(
- 'Успешно обработан платеж YooKassa %s как покупка подписки: пользователь %s, сумма %s₽',
- payment.yookassa_payment_id,
- payment.user_id,
- payment.amount_kopeks / 100,
+ 'Успешно обработан платеж YooKassa как покупка подписки: пользователь , сумма ₽',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ user_id=payment.user_id,
+ amount_kopeks=payment.amount_kopeks / 100,
)
else:
logger.info(
- 'Успешно обработан платеж YooKassa %s: пользователь %s пополнил баланс на %s₽',
- payment.yookassa_payment_id,
- payment.user_id,
- payment.amount_kopeks / 100,
+ 'Успешно обработан платеж YooKassa : пользователь пополнил баланс на ₽',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ user_id=payment.user_id,
+ amount_kopeks=payment.amount_kopeks / 100,
)
# Создаем чек через NaloGO (если NALOGO_ENABLED=true)
@@ -1100,9 +1102,9 @@ class YooKassaPaymentMixin:
except Exception as error:
logger.error(
- 'Ошибка обработки успешного платежа YooKassa %s: %s',
- payment.yookassa_payment_id,
- error,
+ 'Ошибка обработки успешного платежа YooKassa',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ error=error,
)
return False
@@ -1139,9 +1141,9 @@ class YooKassaPaymentMixin:
payment.metadata_json = updated_metadata
except Exception as mark_error: # pragma: no cover - защитный лог
logger.warning(
- 'Не удалось отметить платеж YooKassa %s как завершенный: %s',
- payment.yookassa_payment_id,
- mark_error,
+ 'Не удалось отметить платеж YooKassa как завершенный',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ mark_error=mark_error,
exc_info=True,
)
@@ -1162,8 +1164,9 @@ class YooKassaPaymentMixin:
# Защита от дублей: если у транзакции уже есть чек — не создаём новый
if transaction and getattr(transaction, 'receipt_uuid', None):
logger.info(
- f'Чек для платежа {payment.yookassa_payment_id} уже создан: {transaction.receipt_uuid}, '
- 'пропускаем повторное создание'
+ 'Чек для платежа уже создан: пропускаем повторное создание',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ receipt_uuid=transaction.receipt_uuid,
)
return
@@ -1182,7 +1185,11 @@ class YooKassaPaymentMixin:
)
if receipt_uuid:
- logger.info(f'Чек NaloGO создан для платежа {payment.yookassa_payment_id}: {receipt_uuid}')
+ logger.info(
+ 'Чек NaloGO создан для платежа',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ receipt_uuid=receipt_uuid,
+ )
# Сохраняем receipt_uuid в транзакцию
if transaction:
@@ -1190,16 +1197,18 @@ class YooKassaPaymentMixin:
transaction.receipt_uuid = receipt_uuid
transaction.receipt_created_at = datetime.utcnow()
await db.commit()
- logger.debug(f'Чек {receipt_uuid} привязан к транзакции {transaction.id}')
+ logger.debug(
+ 'Чек привязан к транзакции', receipt_uuid=receipt_uuid, transaction_id=transaction.id
+ )
except Exception as save_error:
- logger.warning(f'Не удалось сохранить receipt_uuid в транзакцию: {save_error}')
+ logger.warning('Не удалось сохранить receipt_uuid в транзакцию', save_error=save_error)
# При временной недоступности чек добавляется в очередь автоматически
except Exception as error:
logger.error(
- 'Ошибка создания чека NaloGO для платежа %s: %s',
- payment.yookassa_payment_id,
- error,
+ 'Ошибка создания чека NaloGO для платежа',
+ yookassa_payment_id=payment.yookassa_payment_id,
+ error=error,
exc_info=True,
)
@@ -1213,7 +1222,7 @@ class YooKassaPaymentMixin:
yookassa_payment_id = event_object.get('id')
if not yookassa_payment_id:
- logger.warning('Webhook без payment id: %s', event)
+ logger.warning('Webhook без payment id', event=event)
return False
remote_data: dict[str, Any] | None = None
@@ -1224,9 +1233,9 @@ class YooKassaPaymentMixin:
)
except Exception as error: # pragma: no cover - диагностический лог
logger.warning(
- 'Не удалось запросить актуальный статус платежа YooKassa %s: %s',
- yookassa_payment_id,
- error,
+ 'Не удалось запросить актуальный статус платежа YooKassa',
+ yookassa_payment_id=yookassa_payment_id,
+ error=error,
exc_info=True,
)
@@ -1235,10 +1244,10 @@ class YooKassaPaymentMixin:
event_object = self._merge_remote_yookassa_payload(event_object, remote_data)
if previous_status and event_object.get('status') != previous_status:
logger.info(
- 'Статус платежа YooKassa %s скорректирован по данным API: %s → %s',
- yookassa_payment_id,
- previous_status,
- event_object.get('status'),
+ 'Статус платежа YooKassa скорректирован по данным API: →',
+ yookassa_payment_id=yookassa_payment_id,
+ previous_status=previous_status,
+ event_object=event_object.get('status'),
)
event['object'] = event_object
@@ -1246,13 +1255,12 @@ class YooKassaPaymentMixin:
payment = await payment_module.get_yookassa_payment_by_id(db, yookassa_payment_id)
if not payment:
- logger.warning('Локальный платеж для YooKassa id %s не найден', yookassa_payment_id)
+ logger.warning('Локальный платеж для YooKassa id не найден', yookassa_payment_id=yookassa_payment_id)
payment = await self._restore_missing_yookassa_payment(db, event_object)
if not payment:
logger.error(
- 'Не удалось восстановить локальную запись платежа YooKassa %s',
- yookassa_payment_id,
+ 'Не удалось восстановить локальную запись платежа YooKassa', yookassa_payment_id=yookassa_payment_id
)
return False
@@ -1274,11 +1282,7 @@ class YooKassaPaymentMixin:
tzinfo=None
)
except Exception as error:
- logger.debug(
- 'Не удалось распарсить captured_at=%s: %s',
- captured_at_raw,
- error,
- )
+ logger.debug('Не удалось распарсить captured_at', captured_at_raw=captured_at_raw, error=error)
await db.commit()
await db.refresh(payment)
@@ -1287,9 +1291,9 @@ class YooKassaPaymentMixin:
return await self._process_successful_yookassa_payment(db, payment)
logger.info(
- 'Webhook YooKassa обновил платеж %s до статуса %s',
- yookassa_payment_id,
- payment.status,
+ 'Webhook YooKassa обновил платеж до статуса',
+ yookassa_payment_id=yookassa_payment_id,
+ payment_status=payment.status,
)
return True
@@ -1309,8 +1313,8 @@ class YooKassaPaymentMixin:
if user_id_raw is None:
logger.error(
- 'Webhook YooKassa %s не содержит user_id в metadata. Невозможно восстановить платеж.',
- yookassa_payment_id,
+ 'Webhook YooKassa не содержит user_id в metadata. Невозможно восстановить платеж.',
+ yookassa_payment_id=yookassa_payment_id,
)
return None
@@ -1318,9 +1322,9 @@ class YooKassaPaymentMixin:
user_id = int(user_id_raw)
except (TypeError, ValueError):
logger.error(
- 'Webhook YooKassa %s содержит некорректный user_id=%s',
- yookassa_payment_id,
- user_id_raw,
+ 'Webhook YooKassa содержит некорректный user_id',
+ yookassa_payment_id=yookassa_payment_id,
+ user_id_raw=user_id_raw,
)
return None
@@ -1331,17 +1335,17 @@ class YooKassaPaymentMixin:
user = await get_user_by_id(db, user_id)
if not user:
logger.warning(
- 'Webhook YooKassa %s: user_id=%s не найден в БД, пропускаем восстановление платежа',
- yookassa_payment_id,
- user_id,
+ 'Webhook YooKassa : user_id= не найден в БД, пропускаем восстановление платежа',
+ yookassa_payment_id=yookassa_payment_id,
+ user_id=user_id,
)
return None
except Exception as e:
logger.warning(
- 'Webhook YooKassa %s: не удалось проверить user_id=%s: %s',
- yookassa_payment_id,
- user_id,
- e,
+ 'Webhook YooKassa : не удалось проверить user_id',
+ yookassa_payment_id=yookassa_payment_id,
+ user_id=user_id,
+ e=e,
)
return None
@@ -1350,20 +1354,17 @@ class YooKassaPaymentMixin:
currency = (amount_info.get('currency') or 'RUB').upper()
if amount_value is None:
- logger.error(
- 'Webhook YooKassa %s не содержит сумму платежа',
- yookassa_payment_id,
- )
+ logger.error('Webhook YooKassa не содержит сумму платежа', yookassa_payment_id=yookassa_payment_id)
return None
try:
amount_kopeks = int((Decimal(str(amount_value)) * 100).quantize(Decimal(1)))
except (InvalidOperation, ValueError) as error:
logger.error(
- 'Некорректная сумма в webhook YooKassa %s: %s (%s)',
- yookassa_payment_id,
- amount_value,
- error,
+ 'Некорректная сумма в webhook YooKassa',
+ yookassa_payment_id=yookassa_payment_id,
+ amount_value=amount_value,
+ error=error,
)
return None
@@ -1377,10 +1378,10 @@ class YooKassaPaymentMixin:
yookassa_created_at = datetime.fromisoformat(created_at_raw.replace('Z', '+00:00')).replace(tzinfo=None)
except Exception as error: # pragma: no cover - диагностический лог
logger.debug(
- 'Не удалось распарсить created_at=%s для YooKassa %s: %s',
- created_at_raw,
- yookassa_payment_id,
- error,
+ 'Не удалось распарсить created_at= для YooKassa',
+ created_at_raw=created_at_raw,
+ yookassa_payment_id=yookassa_payment_id,
+ error=error,
)
payment_module = import_module('app.services.payment_service')
@@ -1436,7 +1437,7 @@ class YooKassaPaymentMixin:
if isinstance(parsed, dict):
return parsed
except json.JSONDecodeError:
- logger.debug('Не удалось распарсить metadata webhook YooKassa: %s', metadata)
+ logger.debug('Не удалось распарсить metadata webhook YooKassa', metadata=metadata)
return {}
diff --git a/app/services/payment_method_config_service.py b/app/services/payment_method_config_service.py
index 87c6feb0..d6ec3e3d 100644
--- a/app/services/payment_method_config_service.py
+++ b/app/services/payment_method_config_service.py
@@ -1,7 +1,6 @@
"""Service for managing payment method display configurations in cabinet."""
-import logging
-
+import structlog
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -10,7 +9,7 @@ from app.config import settings
from app.database.models import PaymentMethodConfig, PromoGroup
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# ============ Default method definitions ============
@@ -199,7 +198,10 @@ async def ensure_payment_method_configs(db: AsyncSession) -> None:
db.add(config)
await db.commit()
- logger.info(f'Payment method configurations initialized ({len(DEFAULT_METHOD_ORDER)} methods).')
+ logger.info(
+ 'Payment method configurations initialized ( methods).',
+ DEFAULT_METHOD_ORDER_count=len(DEFAULT_METHOD_ORDER),
+ )
return
# Add missing methods (for cases when new methods are added to code)
@@ -207,7 +209,7 @@ async def ensure_payment_method_configs(db: AsyncSession) -> None:
missing_methods = [m for m in DEFAULT_METHOD_ORDER if m not in existing_method_ids]
if missing_methods:
- logger.info(f'Adding missing payment methods: {missing_methods}')
+ logger.info('Adding missing payment methods', missing_methods=missing_methods)
# Get max sort_order to append new methods at the end
max_order_result = await db.execute(select(func.max(PaymentMethodConfig.sort_order)))
max_order = max_order_result.scalar() or 0
@@ -235,7 +237,7 @@ async def ensure_payment_method_configs(db: AsyncSession) -> None:
db.add(config)
await db.commit()
- logger.info(f'Added {len(missing_methods)} missing payment method(s).')
+ logger.info('Added missing payment method(s).', missing_methods_count=len(missing_methods))
# ============ CRUD ============
diff --git a/app/services/payment_service.py b/app/services/payment_service.py
index 29763c81..04f1a443 100644
--- a/app/services/payment_service.py
+++ b/app/services/payment_service.py
@@ -2,9 +2,9 @@
from __future__ import annotations
-import logging
from importlib import import_module
+import structlog
from aiogram import Bot
from app.config import settings
@@ -36,7 +36,7 @@ from app.services.yookassa_service import YooKassaService
from app.utils.currency_converter import currency_converter # noqa: F401
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# --- Совместимость: экспортируем функции, которые активно мокаются в тестах ---
@@ -326,15 +326,15 @@ class PaymentService(
mulenpay_name = settings.get_mulenpay_display_name()
logger.debug(
- 'PaymentService инициализирован (YooKassa=%s, Stars=%s, CryptoBot=%s, Heleket=%s, %s=%s, Pal24=%s, Platega=%s, Wata=%s, CloudPayments=%s)',
- bool(self.yookassa_service),
- bool(self.stars_service),
- bool(self.cryptobot_service),
- bool(self.heleket_service),
- mulenpay_name,
- bool(self.mulenpay_service),
- bool(self.pal24_service),
- bool(self.platega_service),
- bool(self.wata_service),
- bool(self.cloudpayments_service),
+ 'PaymentService инициализирован (YooKassa Stars CryptoBot Heleket Pal24 Platega Wata CloudPayments=)',
+ yookassa_service=bool(self.yookassa_service),
+ stars_service=bool(self.stars_service),
+ cryptobot_service=bool(self.cryptobot_service),
+ heleket_service=bool(self.heleket_service),
+ mulenpay_name=mulenpay_name,
+ mulenpay_service=bool(self.mulenpay_service),
+ pal24_service=bool(self.pal24_service),
+ platega_service=bool(self.platega_service),
+ wata_service=bool(self.wata_service),
+ cloudpayments_service=bool(self.cloudpayments_service),
)
diff --git a/app/services/payment_verification_service.py b/app/services/payment_verification_service.py
index 9d94fe7d..c368f92c 100644
--- a/app/services/payment_verification_service.py
+++ b/app/services/payment_verification_service.py
@@ -3,7 +3,6 @@
from __future__ import annotations
import asyncio
-import logging
import re
from collections import Counter
from collections.abc import Iterable
@@ -11,6 +10,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
+import structlog
from sqlalchemy import desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -35,7 +35,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
PENDING_MAX_AGE = timedelta(hours=24)
@@ -181,9 +181,9 @@ class AutoPaymentVerificationService:
self._task = asyncio.create_task(self._auto_check_loop())
logger.info(
- '🔄 Автопроверка пополнений запущена (каждые %s мин) для: %s',
- interval_minutes,
- display_names,
+ '🔄 Автопроверка пополнений запущена (каждые мин) для',
+ interval_minutes=interval_minutes,
+ display_names=display_names,
)
async def stop(self) -> None:
@@ -211,11 +211,7 @@ class AutoPaymentVerificationService:
except asyncio.CancelledError:
raise
except Exception as error:
- logger.error(
- 'Ошибка автопроверки пополнений: %s',
- error,
- exc_info=True,
- )
+ logger.error('Ошибка автопроверки пополнений', error=error, exc_info=True)
await asyncio.sleep(max(1, interval_minutes) * 60)
except asyncio.CancelledError:
@@ -241,9 +237,7 @@ class AutoPaymentVerificationService:
for method, count in sorted(counts.items(), key=lambda item: method_display_name(item[0]))
)
logger.info(
- '🔄 Автопроверка пополнений: найдено %s инвойсов (%s)',
- len(candidates),
- summary,
+ '🔄 Автопроверка пополнений: найдено инвойсов', candidates_count=len(candidates), summary=summary
)
for record in candidates:
@@ -256,32 +250,32 @@ class AutoPaymentVerificationService:
if not refreshed:
logger.debug(
- 'Автопроверка пополнений: не удалось обновить %s %s',
- method_display_name(record.method),
- record.identifier,
+ 'Автопроверка пополнений: не удалось обновить',
+ method_display_name=method_display_name(record.method),
+ identifier=record.identifier,
)
continue
if refreshed.is_paid and not record.is_paid:
logger.info(
- '✅ %s %s отмечен как оплаченный после автопроверки',
- method_display_name(refreshed.method),
- refreshed.identifier,
+ '✅ отмечен как оплаченный после автопроверки',
+ method_display_name=method_display_name(refreshed.method),
+ identifier=refreshed.identifier,
)
elif refreshed.status != record.status:
logger.info(
- 'ℹ️ %s %s обновлён: %s → %s',
- method_display_name(refreshed.method),
- refreshed.identifier,
- record.status or '—',
- refreshed.status or '—',
+ 'ℹ️ обновлён: →',
+ method_display_name=method_display_name(refreshed.method),
+ identifier=refreshed.identifier,
+ record_status=record.status or '—',
+ refreshed_status=refreshed.status or '—',
)
else:
logger.debug(
- 'Автопроверка пополнений: %s %s без изменений (%s)',
- method_display_name(refreshed.method),
- refreshed.identifier,
- refreshed.status or '—',
+ 'Автопроверка пополнений: без изменений',
+ method_display_name=method_display_name(refreshed.method),
+ identifier=refreshed.identifier,
+ refreshed_status=refreshed.status or '—',
)
if session.in_transaction():
@@ -399,17 +393,17 @@ def _build_record(
) -> PendingPayment | None:
user = getattr(payment, 'user', None)
if user is None:
- logger.debug('Skipping %s payment %s without linked user', method.value, identifier)
+ logger.debug('Skipping payment without linked user', method_value=method.value, identifier=identifier)
return None
created_at = getattr(payment, 'created_at', None)
if not isinstance(created_at, datetime):
- logger.debug('Skipping %s payment %s without valid created_at', method.value, identifier)
+ logger.debug('Skipping payment without valid created_at', method_value=method.value, identifier=identifier)
return None
local_id = getattr(payment, 'id', None)
if local_id is None:
- logger.debug('Skipping %s payment without local id', method.value)
+ logger.debug('Skipping payment without local id', method_value=method.value)
return None
return PendingPayment(
@@ -835,7 +829,7 @@ async def get_payment_record(
return None
await db.refresh(payment, attribute_names=['user'])
if payment.created_at < cutoff:
- logger.debug('YooKassa payment %s is older than cutoff', payment.id)
+ logger.debug('YooKassa payment is older than cutoff', payment_id=payment.id)
return _build_record(
method,
payment,
@@ -918,7 +912,7 @@ async def get_payment_record(
is_paid=bool(transaction.is_completed),
)
- logger.debug('Unsupported payment method requested: %s', method)
+ logger.debug('Unsupported payment method requested', method=method)
return None
@@ -961,7 +955,7 @@ async def run_manual_check(
result = await payment_service.get_kassa_ai_payment_status(db, local_payment_id)
payment = result.get('payment') if result else None
else:
- logger.warning('Manual check requested for unsupported method %s', method)
+ logger.warning('Manual check requested for unsupported method', method=method)
return None
if not payment:
@@ -971,10 +965,10 @@ async def run_manual_check(
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- 'Manual status check failed for %s payment %s: %s',
- method.value,
- local_payment_id,
- error,
+ 'Manual status check failed for payment',
+ method_value=method.value,
+ local_payment_id=local_payment_id,
+ error=error,
exc_info=True,
)
return None
diff --git a/app/services/pinned_message_service.py b/app/services/pinned_message_service.py
index d5f4a91f..d6201683 100644
--- a/app/services/pinned_message_service.py
+++ b/app/services/pinned_message_service.py
@@ -1,7 +1,7 @@
import asyncio
-import logging
from datetime import datetime
+import structlog
from aiogram import Bot
from aiogram.exceptions import (
TelegramBadRequest,
@@ -17,7 +17,7 @@ from app.database.models import PinnedMessage, User, UserStatus
from app.utils.validators import sanitize_html, validate_html_tags
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def get_active_pinned_message(db: AsyncSession) -> PinnedMessage | None:
@@ -76,7 +76,7 @@ async def set_active_pinned_message(
await db.commit()
await db.refresh(pinned_message)
- logger.info('Создано новое закрепленное сообщение #%s', pinned_message.id)
+ logger.info('Создано новое закрепленное сообщение #', pinned_message_id=pinned_message.id)
return pinned_message
@@ -89,7 +89,7 @@ async def deactivate_active_pinned_message(db: AsyncSession) -> PinnedMessage |
pinned_message.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(pinned_message)
- logger.info('Деактивировано закрепленное сообщение #%s', pinned_message.id)
+ logger.info('Деактивировано закрепленное сообщение #', pinned_message_id=pinned_message.id)
return pinned_message
@@ -175,17 +175,13 @@ async def broadcast_pinned_message(
break
except TelegramRetryAfter as retry_error:
delay = min(retry_error.retry_after + 1, 30)
- logger.warning(
- 'RetryAfter for user %s, waiting %s seconds',
- telegram_id,
- delay,
- )
+ logger.warning('RetryAfter for user , waiting seconds', telegram_id=telegram_id, delay=delay)
await asyncio.sleep(delay)
except Exception as send_error:
logger.error(
- 'Ошибка отправки закрепленного сообщения пользователю %s: %s',
- telegram_id,
- send_error,
+ 'Ошибка отправки закрепленного сообщения пользователю',
+ telegram_id=telegram_id,
+ send_error=send_error,
)
failed_count += 1
break
@@ -255,11 +251,7 @@ async def unpin_active_pinned_message(
else:
failed_count += 1
except Exception as error:
- logger.error(
- 'Ошибка открепления сообщения у пользователя %s: %s',
- telegram_id,
- error,
- )
+ logger.error('Ошибка открепления сообщения у пользователя', telegram_id=telegram_id, error=error)
failed_count += 1
for i in range(0, len(recipient_telegram_ids), 40):
@@ -341,17 +333,9 @@ async def _send_and_pin_message(bot: Bot, chat_id: int, pinned_message: PinnedMe
await asyncio.sleep(min(e.retry_after + 1, 30))
raise # Propagate to caller's retry loop
except TelegramBadRequest as error:
- logger.warning(
- 'Некорректный запрос при отправке закрепленного сообщения в чат %s: %s',
- chat_id,
- error,
- )
+ logger.warning('Некорректный запрос при отправке закрепленного сообщения в чат', chat_id=chat_id, error=error)
except Exception as error:
- logger.error(
- 'Не удалось отправить закрепленное сообщение пользователю %s: %s',
- chat_id,
- error,
- )
+ logger.error('Не удалось отправить закрепленное сообщение пользователю', chat_id=chat_id, error=error)
return False
@@ -365,18 +349,18 @@ async def _unpin_message_for_user(bot: Bot, chat_id: int, max_retries: int = 3)
if attempt < max_retries - 1:
delay = min(e.retry_after + 1, 30)
logger.warning(
- 'RetryAfter при откреплении для %s, ожидание %s сек (попытка %d/%d)',
- chat_id,
- delay,
- attempt + 1,
- max_retries,
+ 'RetryAfter при откреплении для , ожидание сек (попытка /)',
+ chat_id=chat_id,
+ delay=delay,
+ attempt=attempt + 1,
+ max_retries=max_retries,
)
await asyncio.sleep(delay)
else:
logger.warning(
- 'Не удалось открепить сообщение у %s после %d попыток (flood control)',
- chat_id,
- max_retries,
+ 'Не удалось открепить сообщение у после попыток (flood control)',
+ chat_id=chat_id,
+ max_retries=max_retries,
)
return False
except TelegramForbiddenError:
@@ -384,10 +368,6 @@ async def _unpin_message_for_user(bot: Bot, chat_id: int, max_retries: int = 3)
except TelegramBadRequest:
return False
except Exception as error:
- logger.error(
- 'Не удалось открепить сообщение у пользователя %s: %s',
- chat_id,
- error,
- )
+ logger.error('Не удалось открепить сообщение у пользователя', chat_id=chat_id, error=error)
return False
return False
diff --git a/app/services/platega_service.py b/app/services/platega_service.py
index ac90ea5c..258cadc7 100644
--- a/app/services/platega_service.py
+++ b/app/services/platega_service.py
@@ -4,16 +4,16 @@ from __future__ import annotations
import asyncio
import json
-import logging
from datetime import datetime, timedelta
from typing import Any
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class PlategaService:
@@ -105,10 +105,7 @@ class PlategaService:
if response.status >= 400:
logger.error(
- 'Platega API error %s %s: %s',
- response.status,
- endpoint,
- raw_text,
+ 'Platega API error', response_status=response.status, endpoint=endpoint, raw_text=raw_text
)
if response.status in self._retryable_statuses and attempt < self._max_retries:
await asyncio.sleep(self._retry_delay * attempt)
@@ -117,29 +114,29 @@ class PlategaService:
return data
except asyncio.CancelledError:
- logger.debug('Platega request cancelled: %s %s', method, endpoint)
+ logger.debug('Platega request cancelled', method=method, endpoint=endpoint)
raise
except TimeoutError as error:
last_error = error
logger.warning(
- 'Platega request timeout (%s %s) attempt %s/%s',
- method,
- endpoint,
- attempt,
- self._max_retries,
+ 'Platega request timeout attempt /',
+ method=method,
+ endpoint=endpoint,
+ attempt=attempt,
+ max_retries=self._max_retries,
)
except aiohttp.ClientError as error:
last_error = error
logger.warning(
- 'Platega client error (%s %s) attempt %s/%s: %s',
- method,
- endpoint,
- attempt,
- self._max_retries,
- error,
+ 'Platega client error attempt /',
+ method=method,
+ endpoint=endpoint,
+ attempt=attempt,
+ max_retries=self._max_retries,
+ error=error,
)
except Exception as error: # pragma: no cover - safety
- logger.exception('Unexpected Platega error: %s', error)
+ logger.exception('Unexpected Platega error', error=error)
return None
if attempt < self._max_retries:
@@ -147,11 +144,11 @@ class PlategaService:
if last_error is not None:
logger.error(
- 'Platega request failed after %s attempts (%s %s): %s',
- self._max_retries,
- method,
- endpoint,
- last_error,
+ 'Platega request failed after attempts',
+ max_retries=self._max_retries,
+ method=method,
+ endpoint=endpoint,
+ last_error=last_error,
)
return None
@@ -169,11 +166,7 @@ class PlategaService:
try:
return json.loads(raw_text), raw_text
except json.JSONDecodeError as error:
- logger.error(
- 'Failed to decode Platega JSON response %s: %s',
- response.url,
- error,
- )
+ logger.error('Failed to decode Platega JSON response', url=response.url, error=error)
return None, raw_text
return None, raw_text
@@ -190,11 +183,7 @@ class PlategaService:
if len(encoded) <= max_bytes:
return cleaned
- logger.debug(
- 'Platega description trimmed from %s to %s bytes',
- len(encoded),
- max_bytes,
- )
+ logger.debug('Platega description trimmed from to bytes', encoded_count=len(encoded), max_bytes=max_bytes)
trimmed_bytes = encoded[:max_bytes]
while True:
@@ -213,5 +202,5 @@ class PlategaService:
delta = timedelta(hours=hours, minutes=minutes, seconds=seconds)
return datetime.utcnow() + delta
except Exception:
- logger.warning('Failed to parse Platega expiresIn value: %s', expires_in)
+ logger.warning('Failed to parse Platega expiresIn value', expires_in=expires_in)
return None
diff --git a/app/services/poll_service.py b/app/services/poll_service.py
index c57893d5..7be577f1 100644
--- a/app/services/poll_service.py
+++ b/app/services/poll_service.py
@@ -1,8 +1,8 @@
import asyncio
-import logging
from collections.abc import Iterable
from types import SimpleNamespace
+import structlog
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
@@ -22,7 +22,7 @@ from app.database.models import (
from app.localization.texts import get_texts
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _build_poll_invitation_text(poll: Poll, language: str) -> str:
@@ -158,18 +158,18 @@ async def send_poll_to_users(
# Проверяем, является ли ошибка связанной с лимитом подключений
if 'too many clients' in str(error).lower():
logger.warning(
- '⚠️ Ограничение на количество подключений к БД: %s пользователю %s',
- poll_id,
- user_snapshot.telegram_id,
+ '⚠️ Ограничение на количество подключений к БД: пользователю',
+ poll_id=poll_id,
+ telegram_id=user_snapshot.telegram_id,
)
# Уменьшаем вероятность переполнения, делая небольшую задержку
await asyncio.sleep(0.1)
else:
logger.error(
- '❌ Ошибка отправки опроса %s пользователю %s: %s',
- poll_id,
- user_snapshot.telegram_id,
- error,
+ '❌ Ошибка отправки опроса пользователю',
+ poll_id=poll_id,
+ telegram_id=user_snapshot.telegram_id,
+ error=error,
)
return 'failed'
diff --git a/app/services/privacy_policy_service.py b/app/services/privacy_policy_service.py
index 8b99171e..1441fea1 100644
--- a/app/services/privacy_policy_service.py
+++ b/app/services/privacy_policy_service.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -11,7 +10,7 @@ from app.database.crud.privacy_policy import (
from app.database.models import PrivacyPolicy
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class PrivacyPolicyService:
@@ -88,7 +87,7 @@ class PrivacyPolicyService:
content,
enable_if_new=enable_if_new,
)
- logger.info('✅ Политика конфиденциальности обновлена для языка %s', lang)
+ logger.info('✅ Политика конфиденциальности обновлена для языка', lang=lang)
return policy
@classmethod
diff --git a/app/services/promo_group_assignment.py b/app/services/promo_group_assignment.py
index 43631d9e..d05341cd 100644
--- a/app/services/promo_group_assignment.py
+++ b/app/services/promo_group_assignment.py
@@ -1,6 +1,6 @@
-import logging
from datetime import datetime
+import structlog
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from sqlalchemy import select
@@ -12,7 +12,7 @@ from app.database.models import PromoGroup, User
from app.services.admin_notification_service import AdminNotificationService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def _notify_admins_about_auto_assignment(
@@ -49,9 +49,9 @@ async def _notify_admins_about_auto_assignment(
)
except Exception as exc:
logger.error(
- 'Ошибка отправки уведомления о автоназначении промогруппы пользователю %s: %s',
- user.telegram_id,
- exc,
+ 'Ошибка отправки уведомления о автоназначении промогруппы пользователю',
+ telegram_id=user.telegram_id,
+ exc=exc,
)
finally:
try:
@@ -96,7 +96,7 @@ async def maybe_assign_promo_group_by_total_spent(
user = await db.get(User, user_id)
if not user:
- logger.debug('Не удалось найти пользователя %s для автовыдачи промогруппы', user_id)
+ logger.debug('Не удалось найти пользователя для автовыдачи промогруппы', user_id=user_id)
return None
# Получаем текущую primary промогруппу
@@ -121,11 +121,11 @@ async def maybe_assign_promo_group_by_total_spent(
if target_threshold <= previous_threshold:
logger.debug(
- "Порог промогруппы '%s' (%s) не превышает ранее назначенный (%s) для пользователя %s",
- target_group.name,
- target_threshold,
- previous_threshold,
- user.telegram_id,
+ "Порог промогруппы '' не превышает ранее назначенный для пользователя",
+ target_group_name=target_group.name,
+ target_threshold=target_threshold,
+ previous_threshold=previous_threshold,
+ telegram_id=user.telegram_id,
)
return None
@@ -134,9 +134,9 @@ async def maybe_assign_promo_group_by_total_spent(
if user.auto_promo_group_assigned and already_has_group:
logger.debug(
- "Пользователь %s уже имеет промогруппу '%s', повторная выдача не требуется",
- user.telegram_id,
- target_group.name,
+ "Пользователь уже имеет промогруппу '', повторная выдача не требуется",
+ telegram_id=user.telegram_id,
+ target_group_name=target_group.name,
)
await sync_user_primary_promo_group(db, user_id)
if target_threshold > previous_threshold:
@@ -154,16 +154,16 @@ async def maybe_assign_promo_group_by_total_spent(
# Добавляем новую промогруппу к существующим
await add_user_to_promo_group(db, user_id, target_group.id, assigned_by='auto')
logger.info(
- "🤖 Пользователю %s добавлена промогруппа '%s' за траты %s ₽",
- user.telegram_id,
- target_group.name,
- total_spent / 100,
+ "🤖 Пользователю добавлена промогруппа '' за траты ₽",
+ telegram_id=user.telegram_id,
+ target_group_name=target_group.name,
+ total_spent=total_spent / 100,
)
else:
logger.info(
- "🤖 Пользователь %s уже имеет промогруппу '%s', отмечаем автоприсвоение",
- user.telegram_id,
- target_group.name,
+ "🤖 Пользователь уже имеет промогруппу '', отмечаем автоприсвоение",
+ telegram_id=user.telegram_id,
+ target_group_name=target_group.name,
)
await db.commit()
@@ -180,10 +180,6 @@ async def maybe_assign_promo_group_by_total_spent(
return target_group
except Exception as exc:
- logger.error(
- 'Ошибка при автоматическом назначении промогруппы пользователю %s: %s',
- user_id,
- exc,
- )
+ logger.error('Ошибка при автоматическом назначении промогруппы пользователю', user_id=user_id, exc=exc)
await db.rollback()
return None
diff --git a/app/services/promo_offer_service.py b/app/services/promo_offer_service.py
index 0e81ec7f..f5455da6 100644
--- a/app/services/promo_offer_service.py
+++ b/app/services/promo_offer_service.py
@@ -1,9 +1,9 @@
from __future__ import annotations
-import logging
from collections.abc import Sequence
from datetime import datetime, timedelta
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -18,7 +18,7 @@ from app.database.models import (
from app.services.subscription_service import SubscriptionService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class PromoOfferService:
@@ -120,8 +120,7 @@ class PromoOfferService:
await db.rollback()
await db.refresh(subscription)
logger.error(
- 'Не удалось синхронизировать временный доступ подписки %s с RemnaWave',
- subscription.id,
+ 'Не удалось синхронизировать временный доступ подписки с RemnaWave', subscription_id=subscription.id
)
return False, None, None, 'remnawave_sync_failed'
@@ -192,9 +191,9 @@ class PromoOfferService:
await self.subscription_service.update_remnawave_user(db, subscription)
except Exception as exc: # pragma: no cover - defensive logging
logger.error(
- 'Ошибка обновления Remnawave при отзыве тестового доступа подписки %s: %s',
- subscription.id,
- exc,
+ 'Ошибка обновления Remnawave при отзыве тестового доступа подписки',
+ subscription_id=subscription.id,
+ exc=exc,
)
await db.commit()
@@ -212,16 +211,16 @@ class PromoOfferService:
)
except Exception as exc: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to record promo offer test access disable log for user %s: %s',
- payload.get('user_id'),
- exc,
+ 'Failed to record promo offer test access disable log for user',
+ payload=payload.get('user_id'),
+ exc=exc,
)
try:
await db.rollback()
except Exception as rollback_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to rollback session after promo offer test access log failure: %s',
- rollback_error,
+ 'Failed to rollback session after promo offer test access log failure',
+ rollback_error=rollback_error,
)
return len(entries)
diff --git a/app/services/promocode_service.py b/app/services/promocode_service.py
index fafcd6a3..4a1ff15f 100644
--- a/app/services/promocode_service.py
+++ b/app/services/promocode_service.py
@@ -1,6 +1,6 @@
-import logging
from typing import Any
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.promo_group import get_promo_group_by_id
@@ -18,7 +18,7 @@ from app.services.remnawave_service import RemnaWaveService
from app.services.subscription_service import SubscriptionService
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class PromoCodeService:
@@ -60,9 +60,9 @@ class PromoCodeService:
recent_count = await count_user_recent_activations(db, user_id, hours=24)
if recent_count >= 5:
logger.warning(
- 'Promo stacking limit: user %s has %d activations in 24h',
- self._format_user_log(user),
- recent_count,
+ 'Promo stacking limit: user has activations in 24h',
+ format_user_log=self._format_user_log(user),
+ recent_count=recent_count,
)
return {'success': False, 'error': 'daily_limit'}
@@ -87,7 +87,9 @@ class PromoCodeService:
await mark_user_as_had_paid_subscription(db, user)
logger.info(
- f'🎯 Пользователь {self._format_user_log(user)} получил платную подписку через промокод {code}'
+ '🎯 Пользователь получил платную подписку через промокод',
+ _format_user_log=self._format_user_log(user),
+ code=code,
)
# Assign promo group if promocode has one
@@ -107,24 +109,33 @@ class PromoCodeService:
)
logger.info(
- f"🎯 Пользователю {self._format_user_log(user)} назначена промогруппа '{promo_group.name}' "
- f'(приоритет: {promo_group.priority}) через промокод {code}'
+ '🎯 Пользователю назначена промогруппа (приоритет: ) через промокод',
+ _format_user_log=self._format_user_log(user),
+ promo_group_name=promo_group.name,
+ priority=promo_group.priority,
+ code=code,
)
# Add to result description
result_description += f'\n🎁 Назначена промогруппа: {promo_group.name}'
else:
logger.warning(
- f'⚠️ Промогруппа ID {promocode.promo_group_id} не найдена для промокода {code}'
+ '⚠️ Промогруппа ID не найдена для промокода',
+ promo_group_id=promocode.promo_group_id,
+ code=code,
)
else:
logger.info(
- f'ℹ️ Пользователь {self._format_user_log(user)} уже имеет промогруппу ID {promocode.promo_group_id}'
+ 'ℹ️ Пользователь уже имеет промогруппу ID',
+ _format_user_log=self._format_user_log(user),
+ promo_group_id=promocode.promo_group_id,
)
except Exception as pg_error:
logger.error(
- f'❌ Ошибка назначения промогруппы для пользователя {self._format_user_log(user)} '
- f'при активации промокода {code}: {pg_error}'
+ '❌ Ошибка назначения промогруппы для пользователя при активации промокода',
+ _format_user_log=self._format_user_log(user),
+ code=code,
+ pg_error=pg_error,
)
# Don't fail the whole promocode activation if promo group assignment fails
@@ -133,7 +144,7 @@ class PromoCodeService:
promocode.current_uses += 1
await db.commit()
- logger.info(f'✅ Пользователь {self._format_user_log(user)} активировал промокод {code}')
+ logger.info('✅ Пользователь активировал промокод', _format_user_log=self._format_user_log(user), code=code)
promocode_data = {
'code': promocode.code,
@@ -155,7 +166,7 @@ class PromoCodeService:
}
except Exception as e:
- logger.error(f'Ошибка активации промокода {code} для пользователя {user_id}: {e}')
+ logger.error('Ошибка активации промокода для пользователя', code=code, user_id=user_id, error=e)
await db.rollback()
return {'success': False, 'error': 'server_error'}
@@ -188,8 +199,11 @@ class PromoCodeService:
if current_discount > 0:
if expires_at is None or expires_at > datetime.utcnow():
logger.warning(
- f'⚠️ Пользователь {self._format_user_log(user)} попытался активировать промокод {promocode.code}, '
- f'но у него уже есть активная скидка {current_discount}% до {expires_at}'
+ '⚠️ Пользователь попытался активировать промокод но у него уже есть активная скидка до',
+ _format_user_log=self._format_user_log(user),
+ code=promocode.code,
+ current_discount=current_discount,
+ expires_at=expires_at,
)
raise ValueError('active_discount_exists')
@@ -214,8 +228,11 @@ class PromoCodeService:
await db.flush()
logger.info(
- f'✅ Пользователю {self._format_user_log(user)} назначена скидка {discount_percent}% '
- f'(срок: {discount_hours} ч.) по промокоду {promocode.code}'
+ '✅ Пользователю назначена скидка (срок: ч.) по промокоду',
+ _format_user_log=self._format_user_log(user),
+ discount_percent=discount_percent,
+ discount_hours=discount_hours,
+ code=promocode.code,
)
if promocode.type == PromoCodeType.BALANCE.value and promocode.balance_bonus_kopeks > 0:
@@ -236,7 +253,9 @@ class PromoCodeService:
effects.append(f'⏰ Подписка продлена на {promocode.subscription_days} дней')
logger.info(
- f'✅ Подписка пользователя {self._format_user_log(user)} продлена на {promocode.subscription_days} дней в RemnaWave с текущими сквадами'
+ '✅ Подписка пользователя продлена на дней в RemnaWave с текущими сквадами',
+ _format_user_log=self._format_user_log(user),
+ subscription_days=promocode.subscription_days,
)
else:
@@ -251,9 +270,9 @@ class PromoCodeService:
trial_squads = [trial_uuid]
except Exception as error:
logger.error(
- 'Не удалось подобрать сквад для подписки по промокоду %s: %s',
- promocode.code,
- error,
+ 'Не удалось подобрать сквад для подписки по промокоду',
+ promocode_code=promocode.code,
+ error=error,
)
forced_devices = None
@@ -278,7 +297,10 @@ class PromoCodeService:
effects.append(f'🎉 Получена подписка на {promocode.subscription_days} дней')
logger.info(
- f'✅ Создана новая подписка для пользователя {self._format_user_log(user)} на {promocode.subscription_days} дней с триал сквадом {trial_squads}'
+ '✅ Создана новая подписка для пользователя на дней с триал сквадом',
+ _format_user_log=self._format_user_log(user),
+ subscription_days=promocode.subscription_days,
+ trial_squads=trial_squads,
)
if promocode.type == PromoCodeType.TRIAL_SUBSCRIPTION.value:
@@ -307,7 +329,9 @@ class PromoCodeService:
effects.append(f'🎁 Активирована тестовая подписка на {trial_days} дней')
logger.info(
- f'✅ Создана триал подписка для пользователя {self._format_user_log(user)} на {trial_days} дней'
+ '✅ Создана триал подписка для пользователя на дней',
+ _format_user_log=self._format_user_log(user),
+ trial_days=trial_days,
)
else:
effects.append('ℹ️ У вас уже есть активная подписка')
@@ -390,16 +414,21 @@ class PromoCodeService:
if has_group:
await remove_user_from_promo_group(db, user_id, promocode.promo_group_id)
logger.info(
- f'Снята промогруппа ID {promocode.promo_group_id} у пользователя '
- f'{self._format_user_log(user)} при деактивации промокода {deactivated_code}'
+ 'Снята промогруппа ID у пользователя при деактивации промокода',
+ promo_group_id=promocode.promo_group_id,
+ _format_user_log=self._format_user_log(user),
+ deactivated_code=deactivated_code,
)
await db.commit()
initiator = 'администратором' if admin_initiated else 'пользователем'
logger.info(
- f'Промокод {deactivated_code} (скидка {current_discount}%) деактивирован '
- f'{initiator} для пользователя {self._format_user_log(user)}'
+ 'Промокод (скидка %) деактивирован для пользователя',
+ deactivated_code=deactivated_code,
+ current_discount=current_discount,
+ initiator=initiator,
+ _format_user_log=self._format_user_log(user),
)
return {
@@ -409,6 +438,6 @@ class PromoCodeService:
}
except Exception as e:
- logger.error(f'Ошибка деактивации промокода для пользователя {user_id}: {e}')
+ logger.error('Ошибка деактивации промокода для пользователя', user_id=user_id, error=e)
await db.rollback()
return {'success': False, 'error': 'server_error'}
diff --git a/app/services/public_offer_service.py b/app/services/public_offer_service.py
index 94ee9f19..02226087 100644
--- a/app/services/public_offer_service.py
+++ b/app/services/public_offer_service.py
@@ -1,6 +1,6 @@
-import logging
from html.parser import HTMLParser
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -12,7 +12,7 @@ from app.database.crud.public_offer import (
from app.database.models import PublicOffer
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class PublicOfferService:
@@ -93,7 +93,7 @@ class PublicOfferService:
content,
enable_if_new=enable_if_new,
)
- logger.info('✅ Публичная оферта обновлена для языка %s', lang)
+ logger.info('✅ Публичная оферта обновлена для языка', lang=lang)
return offer
@classmethod
diff --git a/app/services/referral_contest_service.py b/app/services/referral_contest_service.py
index c39d2354..2a315652 100644
--- a/app/services/referral_contest_service.py
+++ b/app/services/referral_contest_service.py
@@ -1,9 +1,9 @@
import asyncio
-import logging
from collections.abc import Sequence
from datetime import UTC, date, datetime, time, timedelta
from zoneinfo import ZoneInfo
+import structlog
from aiogram import Bot
from aiogram.exceptions import TelegramForbiddenError, TelegramNotFound
from sqlalchemy.ext.asyncio import AsyncSession
@@ -25,7 +25,7 @@ from app.database.database import AsyncSessionLocal
from app.database.models import ReferralContest, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class ReferralContestService:
@@ -71,7 +71,7 @@ class ReferralContestService:
except asyncio.CancelledError:
raise
except Exception as exc:
- logger.error('Ошибка сервиса конкурсов: %s', exc)
+ logger.error('Ошибка сервиса конкурсов', exc=exc)
await asyncio.sleep(self._poll_interval_seconds)
except asyncio.CancelledError:
@@ -93,12 +93,7 @@ class ReferralContestService:
except asyncio.CancelledError:
raise
except Exception as exc:
- logger.error(
- 'Ошибка обработки конкурса %s (%s): %s',
- contest.id,
- contest.title,
- exc,
- )
+ logger.error('Ошибка обработки конкурса', contest_id=contest.id, title=contest.title, exc=exc)
async def _maybe_send_daily_summary(
self,
@@ -204,7 +199,7 @@ class ReferralContestService:
)
if not leaderboard:
- logger.info('Конкурс %s: пока нет участников', contest.id)
+ logger.info('Конкурс : пока нет участников', contest_id=contest.id)
if is_final:
await mark_final_summary_sent(db, contest)
@@ -255,18 +250,17 @@ class ReferralContestService:
# Skip email-only users (no telegram_id)
if not user.telegram_id:
- logger.debug(f'Skipping contest notification for email-only user {user.id}')
+ logger.debug('Skipping contest notification for email-only user', user_id=user.id)
continue
try:
await self.bot.send_message(user.telegram_id, text, disable_web_page_preview=True)
except (TelegramForbiddenError, TelegramNotFound):
logger.info(
- 'Не удалось отправить сообщение участнику %s (вероятно, блокировка)',
- user.telegram_id,
+ 'Не удалось отправить сообщение участнику (вероятно, блокировка)', telegram_id=user.telegram_id
)
except Exception as exc:
- logger.error('Ошибка отправки участнику конкурса %s: %s', user.telegram_id, exc)
+ logger.error('Ошибка отправки участнику конкурса', telegram_id=user.telegram_id, exc=exc)
async def _notify_admins(
self,
@@ -314,7 +308,7 @@ class ReferralContestService:
message_thread_id=settings.ADMIN_NOTIFICATIONS_TOPIC_ID,
)
except Exception as exc:
- logger.error('Не удалось отправить админскую сводку конкурса: %s', exc)
+ logger.error('Не удалось отправить админскую сводку конкурса', exc=exc)
async def _notify_public_channel(
self,
@@ -364,9 +358,9 @@ class ReferralContestService:
disable_web_page_preview=True,
)
except (TelegramForbiddenError, TelegramNotFound):
- logger.info('Не удалось отправить сводку конкурса в канал %s', channel_id_raw)
+ logger.info('Не удалось отправить сводку конкурса в канал', channel_id_raw=channel_id_raw)
except Exception as exc:
- logger.error('Ошибка отправки сводки конкурса в канал %s: %s', channel_id_raw, exc)
+ logger.error('Ошибка отправки сводки конкурса в канал', channel_id_raw=channel_id_raw, exc=exc)
def _build_participant_message(
self,
@@ -483,7 +477,7 @@ class ReferralContestService:
try:
return ZoneInfo(tz_name)
except Exception:
- logger.warning('Не удалось загрузить TZ %s, используем UTC', tz_name)
+ logger.warning('Не удалось загрузить TZ , используем UTC', tz_name=tz_name)
return ZoneInfo('UTC')
def _parse_times(self, times_str: str | None) -> list[time]:
@@ -549,12 +543,12 @@ class ReferralContestService:
if user_created_at < contest_start or user_created_at > contest_end:
logger.debug(
- 'Реферал %s зарегистрирован вне периода конкурса %s (создан %s, период %s - %s)',
- user.id,
- contest.id,
- user_created_at,
- contest_start,
- contest_end,
+ 'Реферал зарегистрирован вне периода конкурса (создан , период -)',
+ user_id=user.id,
+ contest_id=contest.id,
+ user_created_at=user_created_at,
+ contest_start=contest_start,
+ contest_end=contest_end,
)
continue
@@ -568,13 +562,13 @@ class ReferralContestService:
)
if event:
logger.info(
- 'Записан зачёт конкурса %s: реферер %s, реферал %s',
- contest.id,
- user.referred_by_id,
- user.id,
+ 'Записан зачёт конкурса : реферер , реферал',
+ contest_id=contest.id,
+ referred_by_id=user.referred_by_id,
+ user_id=user.id,
)
except Exception as exc:
- logger.error('Не удалось записать зачёт конкурса %s: %s', contest.id, exc)
+ logger.error('Не удалось записать зачёт конкурса', contest_id=contest.id, exc=exc)
async def on_referral_registration(
self,
@@ -610,12 +604,12 @@ class ReferralContestService:
if user_created_at < contest_start or user_created_at > contest_end:
logger.debug(
- 'Реферал %s зарегистрирован вне периода конкурса %s (создан %s, период %s - %s)',
- user.id,
- contest.id,
- user_created_at,
- contest_start,
- contest_end,
+ 'Реферал зарегистрирован вне периода конкурса (создан , период -)',
+ user_id=user.id,
+ contest_id=contest.id,
+ user_created_at=user_created_at,
+ contest_start=contest_start,
+ contest_end=contest_end,
)
continue
@@ -629,13 +623,13 @@ class ReferralContestService:
)
if event:
logger.info(
- 'Записан зачёт конкурса регистрации %s: реферер %s, реферал %s',
- contest.id,
- user.referred_by_id,
- user.id,
+ 'Записан зачёт конкурса регистрации : реферер , реферал',
+ contest_id=contest.id,
+ referred_by_id=user.referred_by_id,
+ user_id=user.id,
)
except Exception as exc:
- logger.error('Не удалось записать зачёт регистрации для конкурса %s: %s', contest.id, exc)
+ logger.error('Не удалось записать зачёт регистрации для конкурса', contest_id=contest.id, exc=exc)
async def sync_contest(
self,
@@ -653,15 +647,15 @@ class ReferralContestService:
stats = await sync_contest_events(db, contest_id)
if 'error' not in stats:
logger.info(
- 'Синхронизация конкурса %s: создано %s, обновлено %s, пропущено %s',
- contest_id,
- stats.get('created', 0),
- stats.get('updated', 0),
- stats.get('skipped', 0),
+ 'Синхронизация конкурса : создано , обновлено , пропущено',
+ contest_id=contest_id,
+ stats=stats.get('created', 0),
+ stats_2=stats.get('updated', 0),
+ stats_3=stats.get('skipped', 0),
)
return stats
except Exception as exc:
- logger.error('Ошибка синхронизации конкурса %s: %s', contest_id, exc)
+ logger.error('Ошибка синхронизации конкурса', contest_id=contest_id, exc=exc)
return {'error': str(exc)}
async def cleanup_contest(
@@ -680,14 +674,14 @@ class ReferralContestService:
stats = await cleanup_invalid_contest_events(db, contest_id)
if 'error' not in stats:
logger.info(
- 'Очистка конкурса %s: удалено %s невалидных событий, осталось %s',
- contest_id,
- stats.get('deleted', 0),
- stats.get('remaining', 0),
+ 'Очистка конкурса : удалено невалидных событий, осталось',
+ contest_id=contest_id,
+ stats=stats.get('deleted', 0),
+ stats_2=stats.get('remaining', 0),
)
return stats
except Exception as exc:
- logger.error('Ошибка очистки конкурса %s: %s', contest_id, exc)
+ logger.error('Ошибка очистки конкурса', contest_id=contest_id, exc=exc)
return {'error': str(exc)}
diff --git a/app/services/referral_diagnostics_service.py b/app/services/referral_diagnostics_service.py
index d7320f1f..31e540f9 100644
--- a/app/services/referral_diagnostics_service.py
+++ b/app/services/referral_diagnostics_service.py
@@ -7,13 +7,13 @@
- Выявление потерянных рефералов
"""
-import logging
import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -23,7 +23,7 @@ from app.database.crud.user import add_user_balance
from app.database.models import ReferralEarning, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@dataclass
@@ -314,13 +314,13 @@ class ReferralDiagnosticsService:
mtime = datetime.fromtimestamp(path.stat().st_mtime).date()
is_fresh = mtime >= today - timedelta(days=1)
candidates.append((path, is_fresh, path.stat().st_mtime))
- logger.info(f'📁 Найден лог: {path} (свежий: {is_fresh})')
+ logger.info('📁 Найден лог: (свежий: )', path=path, is_fresh=is_fresh)
candidates.sort(key=lambda x: (not x[1], -x[2]))
if candidates:
selected = candidates[0][0]
- logger.info(f'✅ Выбран лог-файл: {selected}')
+ logger.info('✅ Выбран лог-файл', selected=selected)
return selected
return Path('logs/current/bot.log')
@@ -378,7 +378,7 @@ class ReferralDiagnosticsService:
Returns:
DiagnosticReport с результатами анализа всего файла
"""
- logger.info(f'📂 Начинаю анализ файла: {file_path}')
+ logger.info('📂 Начинаю анализ файла', file_path=file_path)
# Парсим весь файл без фильтра по дате
# Используем широкий диапазон дат (все время)
@@ -402,8 +402,10 @@ class ReferralDiagnosticsService:
lost_referrals = await self._find_lost_referrals(db, list(user_clicks.values()))
logger.info(
- f'✅ Анализ файла завершён: строк={total_lines}, '
- f'реф-кликов={len(clicks)}, потерянных={len(lost_referrals)}'
+ '✅ Анализ файла завершён: строк=, реф-кликов=, потерянных',
+ total_lines=total_lines,
+ clicks_count=len(clicks),
+ lost_referrals_count=len(lost_referrals),
)
return DiagnosticReport(
@@ -429,11 +431,11 @@ class ReferralDiagnosticsService:
lines_in_period = 0
if not self.log_path.exists():
- logger.warning(f'❌ Лог-файл не найден: {self.log_path}')
+ logger.warning('❌ Лог-файл не найден', log_path=self.log_path)
return clicks, 0, 0
file_size = self.log_path.stat().st_size
- logger.info(f'📂 Читаю лог-файл: {self.log_path} ({file_size / 1024 / 1024:.2f} MB)')
+ logger.info('📂 Читаю лог-файл: ( MB)', log_path=self.log_path, file_size=round(file_size / 1024 / 1024, 2))
# Паттерн timestamp
timestamp_pattern = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+ - .+ - .+ - (.+)$')
@@ -505,9 +507,14 @@ class ReferralDiagnosticsService:
break
except Exception as e:
- logger.error(f'Ошибка парсинга логов: {e}', exc_info=True)
+ logger.error('Ошибка парсинга логов', error=e, exc_info=True)
- logger.info(f'📊 Парсинг: строк={total_lines}, за период={lines_in_period}, реф-кликов={len(clicks)}')
+ logger.info(
+ '📊 Парсинг: строк=, за период=, реф-кликов',
+ total_lines=total_lines,
+ lines_in_period=lines_in_period,
+ clicks_count=len(clicks),
+ )
return clicks, total_lines, lines_in_period
async def _find_lost_referrals(self, db: AsyncSession, clicks: list[ReferralClick]) -> list[LostReferral]:
@@ -542,7 +549,12 @@ class ReferralDiagnosticsService:
# Пользователь был создан ДО клика по реф-ссылке
# Это старый пользователь, который просто зашёл по чужой ссылке
is_lost = False
- logger.debug(f'⏭️ Пропускаем {click.telegram_id}: создан {user.created_at} < клик {click.timestamp}')
+ logger.debug(
+ '⏭️ Пропускаем создан < клик',
+ telegram_id=click.telegram_id,
+ created_at=user.created_at,
+ timestamp=click.timestamp,
+ )
elif user.referred_by_id is None:
# Пользователь в БД, но без реферера (и создан после клика)
is_lost = True
@@ -567,7 +579,7 @@ class ReferralDiagnosticsService:
)
)
- logger.info(f'🔍 Найдено потерянных рефералов: {len(lost)}')
+ logger.info('🔍 Найдено потерянных рефералов', lost_count=len(lost))
return lost
async def _add_to_active_contests(
@@ -610,7 +622,9 @@ class ReferralDiagnosticsService:
contest_end = contest.end_at if contest.end_at.tzinfo is None else contest.end_at.replace(tzinfo=None)
if user_created_at < contest_start or user_created_at > contest_end:
- logger.debug(f'Реферал {referral.id} зарегистрирован вне периода конкурса {contest.id}')
+ logger.debug(
+ 'Реферал зарегистрирован вне периода конкурса', referral_id=referral.id, contest_id=contest.id
+ )
continue
event = await add_contest_event(
@@ -623,11 +637,13 @@ class ReferralDiagnosticsService:
)
if event:
logger.info(
- f'🏆 Восстановленный реферал добавлен в конкурс {contest.id}: '
- f'реферер {referrer.id}, реферал {referral.id}'
+ '🏆 Восстановленный реферал добавлен в конкурс реферер реферал',
+ contest_id=contest.id,
+ referrer_id=referrer.id,
+ referral_id=referral.id,
)
except Exception as exc:
- logger.error(f'Не удалось добавить в конкурс {contest.id}: {exc}')
+ logger.error('Не удалось добавить в конкурс', contest_id=contest.id, error=exc)
# Также проверяем конкурсы по регистрации (если есть)
reg_contests = await get_contests_for_events(db, now_utc, contest_types=['referral_registered'])
@@ -656,9 +672,9 @@ class ReferralDiagnosticsService:
event_type='restored_referral_registration',
)
if event:
- logger.info(f'🏆 Восстановленный реферал (регистрация) добавлен в конкурс {contest.id}')
+ logger.info('🏆 Восстановленный реферал (регистрация) добавлен в конкурс', contest_id=contest.id)
except Exception as exc:
- logger.error(f'Не удалось добавить в конкурс регистрации {contest.id}: {exc}')
+ logger.error('Не удалось добавить в конкурс регистрации', contest_id=contest.id, error=exc)
async def fix_lost_referrals(
self, db: AsyncSession, lost_referrals: list[LostReferral], apply: bool = False
@@ -718,7 +734,11 @@ class ReferralDiagnosticsService:
if user.referred_by_id != referrer.id:
if apply:
user.referred_by_id = referrer.id
- logger.info(f'✅ Установлен referred_by_id={referrer.id} для пользователя {user.telegram_id}')
+ logger.info(
+ '✅ Установлен referred_by_id= для пользователя',
+ referrer_id=referrer.id,
+ telegram_id=user.telegram_id,
+ )
detail.referred_by_set = True
report.users_fixed += 1
@@ -769,8 +789,9 @@ class ReferralDiagnosticsService:
)
user.has_made_first_topup = True
logger.info(
- f'💰 Начислен бонус рефералу {user.telegram_id}: '
- f'{settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS / 100}₽'
+ '💰 Начислен бонус рефералу ₽',
+ telegram_id=user.telegram_id,
+ REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS / 100,
)
# 4. Начисляем бонус рефереру
@@ -804,8 +825,9 @@ class ReferralDiagnosticsService:
)
logger.info(
- f'💰 Начислен бонус рефереру {referrer.telegram_id or referrer.id}: '
- f'{inviter_bonus / 100}₽'
+ '💰 Начислен бонус рефереру ₽',
+ telegram_id=referrer.telegram_id or referrer.id,
+ inviter_bonus=inviter_bonus / 100,
)
# Добавляем в активные конкурсы рефералов
@@ -816,7 +838,7 @@ class ReferralDiagnosticsService:
report.details.append(detail)
except Exception as e:
- logger.error(f'❌ Ошибка исправления реферала {lost.telegram_id}: {e}', exc_info=True)
+ logger.error('❌ Ошибка исправления реферала', telegram_id=lost.telegram_id, error=e, exc_info=True)
detail.error = str(e)
report.errors += 1
report.details.append(detail)
@@ -824,11 +846,13 @@ class ReferralDiagnosticsService:
if apply:
await db.commit()
logger.info(
- f'✅ Исправлено рефералов: {report.users_fixed}, '
- f'начислено бонусов: {report.bonuses_to_referrals / 100}₽ + {report.bonuses_to_referrers / 100}₽'
+ '✅ Исправлено рефералов: начислено бонусов: ₽ + ₽',
+ users_fixed=report.users_fixed,
+ bonuses_to_referrals=report.bonuses_to_referrals / 100,
+ bonuses_to_referrers=report.bonuses_to_referrers / 100,
)
else:
- logger.info(f'📋 Предпросмотр: {report.users_fixed} рефералов будут исправлены')
+ logger.info('📋 Предпросмотр: рефералов будут исправлены', users_fixed=report.users_fixed)
return report
@@ -932,9 +956,10 @@ class ReferralDiagnosticsService:
report.total_missing_to_referrers += missing.referrer_bonus_amount
logger.info(
- f'📊 Проверка бонусов: {report.total_referrals_checked} рефералов, '
- f'{report.referrals_with_topup} с пополнением, '
- f'{len(report.missing_bonuses)} без бонусов'
+ '📊 Проверка бонусов: рефералов, с пополнением, без бонусов',
+ total_referrals_checked=report.total_referrals_checked,
+ referrals_with_topup=report.referrals_with_topup,
+ missing_bonuses_count=len(report.missing_bonuses),
)
return report
@@ -1007,7 +1032,9 @@ class ReferralDiagnosticsService:
)
referral.has_made_first_topup = True
logger.info(
- f'💰 Начислен бонус рефералу {referral.telegram_id}: {missing.referral_bonus_amount / 100}₽'
+ '💰 Начислен бонус рефералу ₽',
+ telegram_id=referral.telegram_id,
+ referral_bonus_amount=missing.referral_bonus_amount / 100,
)
# Начисляем бонус рефереру
@@ -1034,7 +1061,9 @@ class ReferralDiagnosticsService:
reason='referral_first_topup',
)
logger.info(
- f'💰 Начислен бонус рефереру {referrer.telegram_id}: {missing.referrer_bonus_amount / 100}₽'
+ '💰 Начислен бонус рефереру ₽',
+ telegram_id=referrer.telegram_id,
+ referrer_bonus_amount=missing.referrer_bonus_amount / 100,
)
# Добавляем в активные конкурсы рефералов
@@ -1044,7 +1073,7 @@ class ReferralDiagnosticsService:
report.details.append(detail)
except Exception as e:
- logger.error(f'❌ Ошибка начисления бонуса: {e}', exc_info=True)
+ logger.error('❌ Ошибка начисления бонуса', error=e, exc_info=True)
detail.error = str(e)
report.errors += 1
report.details.append(detail)
@@ -1052,8 +1081,9 @@ class ReferralDiagnosticsService:
if apply:
await db.commit()
logger.info(
- f'✅ Начислено бонусов: {report.bonuses_to_referrals / 100}₽ рефералам + '
- f'{report.bonuses_to_referrers / 100}₽ рефереерам'
+ '✅ Начислено бонусов: ₽ рефералам + ₽ рефереерам',
+ bonuses_to_referrals=report.bonuses_to_referrals / 100,
+ bonuses_to_referrers=report.bonuses_to_referrers / 100,
)
return report
diff --git a/app/services/referral_service.py b/app/services/referral_service.py
index 8fbcaf81..78710a1b 100644
--- a/app/services/referral_service.py
+++ b/app/services/referral_service.py
@@ -1,5 +1,4 @@
-import logging
-
+import structlog
from aiogram import Bot
from sqlalchemy import delete
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +13,7 @@ from app.services.notification_delivery_service import (
from app.utils.user_utils import get_effective_referral_commission_percent
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
async def send_referral_notification(
@@ -46,18 +45,18 @@ async def send_referral_notification(
telegram_message=message,
)
if success:
- logger.info(f'✅ Email уведомление о реферале отправлено пользователю {user.id}')
+ logger.info('✅ Email уведомление о реферале отправлено пользователю', user_id=user.id)
else:
- logger.warning(f'⚠️ Не удалось отправить email уведомление пользователю {user.id}')
+ logger.warning('⚠️ Не удалось отправить email уведомление пользователю', user_id=user.id)
else:
logger.debug('Пропуск уведомления: пользователь без telegram_id и без User object')
return
try:
await bot.send_message(telegram_id, message, parse_mode='HTML')
- logger.info(f'✅ Уведомление отправлено пользователю {telegram_id}')
+ logger.info('✅ Уведомление отправлено пользователю', telegram_id=telegram_id)
except Exception as e:
- logger.error(f'❌ Ошибка отправки уведомления пользователю {telegram_id}: {e}')
+ logger.error('❌ Ошибка отправки уведомления пользователю', telegram_id=telegram_id, error=e)
async def process_referral_registration(db: AsyncSession, new_user_id: int, referrer_id: int, bot: Bot = None):
@@ -66,11 +65,13 @@ async def process_referral_registration(db: AsyncSession, new_user_id: int, refe
referrer = await get_user_by_id(db, referrer_id)
if not new_user or not referrer:
- logger.error(f'Пользователи не найдены: new_user_id={new_user_id}, referrer_id={referrer_id}')
+ logger.error(
+ 'Пользователи не найдены: new_user_id=, referrer_id', new_user_id=new_user_id, referrer_id=referrer_id
+ )
return False
if new_user.referred_by_id != referrer_id:
- logger.error(f'Пользователь {new_user_id} не привязан к рефереру {referrer_id}')
+ logger.error('Пользователь не привязан к рефереру', new_user_id=new_user_id, referrer_id=referrer_id)
return False
await create_referral_earning(
@@ -82,7 +83,7 @@ async def process_referral_registration(db: AsyncSession, new_user_id: int, refe
await referral_contest_service.on_referral_registration(db, new_user_id)
except Exception as exc:
- logger.debug('Не удалось записать конкурсную регистрацию: %s', exc)
+ logger.debug('Не удалось записать конкурсную регистрацию', exc=exc)
if bot:
commission_percent = get_effective_referral_commission_percent(referrer)
@@ -108,12 +109,14 @@ async def process_referral_registration(db: AsyncSession, new_user_id: int, refe
)
logger.info(
- f'✅ Зарегистрирован реферал {new_user_id} для {referrer_id}. Бонусы будут выданы после пополнения.'
+ '✅ Зарегистрирован реферал для . Бонусы будут выданы после пополнения.',
+ new_user_id=new_user_id,
+ referrer_id=referrer_id,
)
return True
except Exception as e:
- logger.error(f'Ошибка обработки реферальной регистрации: {e}')
+ logger.error('Ошибка обработки реферальной регистрации', error=e)
return False
@@ -121,12 +124,12 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
try:
user = await get_user_by_id(db, user_id)
if not user or not user.referred_by_id:
- logger.info(f'Пользователь {user_id} не является рефералом')
+ logger.info('Пользователь не является рефералом', user_id=user_id)
return True
referrer = await get_user_by_id(db, user.referred_by_id)
if not referrer:
- logger.error(f'Реферер {user.referred_by_id} не найден')
+ logger.error('Реферер не найден', referred_by_id=user.referred_by_id)
return False
commission_percent = get_effective_referral_commission_percent(referrer)
@@ -138,9 +141,9 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
if not user.has_made_first_topup:
if not qualifies_for_first_bonus:
logger.info(
- 'Пополнение %s на %s₽ меньше минимума для первого бонуса, но комиссия будет начислена',
- user_id,
- topup_amount_kopeks / 100,
+ 'Пополнение на ₽ меньше минимума для первого бонуса, но комиссия будет начислена',
+ user_id=user_id,
+ topup_amount_kopeks=topup_amount_kopeks / 100,
)
if commission_amount > 0:
@@ -161,9 +164,9 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
)
logger.info(
- '💰 Комиссия с пополнения: %s получил %s₽ (до первого бонуса)',
- referrer.telegram_id,
- commission_amount / 100,
+ '💰 Комиссия с пополнения: получил ₽ (до первого бонуса)',
+ telegram_id=referrer.telegram_id,
+ commission_amount=commission_amount / 100,
)
if bot:
@@ -198,9 +201,9 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
)
)
await db.commit()
- logger.info(f"🗑️ Удалена запись 'ожидание пополнения' для реферала {user.id}")
+ logger.info("🗑️ Удалена запись 'ожидание пополнения' для реферала", user_id=user.id)
except Exception as e:
- logger.error(f'Ошибка удаления записи ожидания: {e}')
+ logger.error('Ошибка удаления записи ожидания', error=e)
if settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS > 0:
await add_user_balance(
@@ -210,7 +213,11 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
'Бонус за первое пополнение по реферальной программе',
bot=bot,
)
- logger.info(f'💰 Реферал {user.id} получил бонус {settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS / 100}₽')
+ logger.info(
+ '💰 Реферал получил бонус ₽',
+ user_id=user.id,
+ REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS / 100,
+ )
if bot:
bonus_notification = (
@@ -243,7 +250,7 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
reason='referral_first_topup',
)
referrer_id = referrer.telegram_id or referrer.email or f'user#{referrer.id}'
- logger.info(f'💰 Реферер {referrer_id} получил бонус {inviter_bonus / 100}₽')
+ logger.info('💰 Реферер получил бонус ₽', referrer_id=referrer_id, inviter_bonus=inviter_bonus / 100)
if bot:
inviter_bonus_notification = (
@@ -279,7 +286,11 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
)
referrer_id = referrer.telegram_id or referrer.email or f'user#{referrer.id}'
- logger.info(f'💰 Комиссия с пополнения: {referrer_id} получил {commission_amount / 100}₽')
+ logger.info(
+ '💰 Комиссия с пополнения: получил ₽',
+ referrer_id=referrer_id,
+ commission_amount=commission_amount / 100,
+ )
if bot:
commission_notification = (
@@ -302,7 +313,7 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
return True
except Exception as e:
- logger.error(f'Ошибка обработки пополнения реферала: {e}')
+ logger.error('Ошибка обработки пополнения реферала', error=e)
return False
@@ -316,7 +327,7 @@ async def process_referral_purchase(
referrer = await get_user_by_id(db, user.referred_by_id)
if not referrer:
- logger.error(f'Реферер {user.referred_by_id} не найден')
+ logger.error('Реферер не найден', referred_by_id=user.referred_by_id)
return False
commission_percent = get_effective_referral_commission_percent(referrer)
@@ -338,7 +349,9 @@ async def process_referral_purchase(
)
referrer_id = referrer.telegram_id or referrer.email or f'user#{referrer.id}'
- logger.info(f'💰 Комиссия с покупки: {referrer_id} получил {commission_amount / 100}₽')
+ logger.info(
+ '💰 Комиссия с покупки: получил ₽', referrer_id=referrer_id, commission_amount=commission_amount / 100
+ )
if bot:
purchase_commission_notification = (
@@ -361,13 +374,13 @@ async def process_referral_purchase(
if not user.has_had_paid_subscription:
user.has_had_paid_subscription = True
await db.commit()
- logger.info(f'✅ Пользователь {user_id} отмечен как имевший платную подписку')
+ logger.info('✅ Пользователь отмечен как имевший платную подписку', user_id=user_id)
return True
except Exception as e:
- logger.error(f'Ошибка обработки покупки реферала: {e}')
+ logger.error('Ошибка обработки покупки реферала', error=e)
import traceback
- logger.error(f'Полный traceback: {traceback.format_exc()}')
+ logger.error('Полный traceback', format_exc=traceback.format_exc())
return False
diff --git a/app/services/referral_withdrawal_service.py b/app/services/referral_withdrawal_service.py
index eb7aebc8..9e2d9fc3 100644
--- a/app/services/referral_withdrawal_service.py
+++ b/app/services/referral_withdrawal_service.py
@@ -4,9 +4,9 @@
"""
import json
-import logging
from datetime import datetime, timedelta
+import structlog
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class ReferralWithdrawalService:
diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py
index e93eb3f4..460ac7f7 100644
--- a/app/services/remnawave_service.py
+++ b/app/services/remnawave_service.py
@@ -1,5 +1,4 @@
import asyncio
-import logging
import re
from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import asdict, is_dataclass
@@ -7,6 +6,7 @@ from datetime import datetime, timedelta
from typing import Any, Optional
from zoneinfo import ZoneInfo
+import structlog
from sqlalchemy import String, and_, cast, delete, func, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -40,7 +40,7 @@ from app.utils.subscription_utils import (
from app.utils.timezone import get_local_timezone
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _get_user_traffic_bytes(panel_user: dict[str, Any]) -> int:
@@ -198,10 +198,10 @@ class RemnaWaveService:
conflicting_user = uuid_map.get(panel_uuid)
if conflicting_user and conflicting_user is not user:
logger.warning(
- '♻️ Обнаружен конфликт UUID %s между пользователями %s и %s. Сбрасываем у старой записи.',
- panel_uuid,
- getattr(conflicting_user, 'telegram_id', '?'),
- getattr(user, 'telegram_id', '?'),
+ '♻️ Обнаружен конфликт UUID между пользователями и . Сбрасываем у старой записи.',
+ panel_uuid=panel_uuid,
+ getattr=getattr(conflicting_user, 'telegram_id', '?'),
+ getattr_2=getattr(user, 'telegram_id', '?'),
)
mutation.set_user_uuid(conflicting_user, None)
mutation.set_user_updated_at(conflicting_user, datetime.utcnow())
@@ -215,10 +215,10 @@ class RemnaWaveService:
mutation.set_map_entry(panel_uuid, user)
logger.info(
- '🔁 Обновлен RemnaWave UUID пользователя %s: %s → %s',
- getattr(user, 'telegram_id', '?'),
- current_uuid,
- panel_uuid,
+ '🔁 Обновлен RemnaWave UUID пользователя : →',
+ getattr=getattr(user, 'telegram_id', '?'),
+ current_uuid=current_uuid,
+ panel_uuid=panel_uuid,
)
if mutation.has_changes():
@@ -274,11 +274,11 @@ class RemnaWaveService:
else:
utc_normalized = parsed_date
- logger.debug(f'Успешно распарсена дата: {date_str} -> {utc_normalized} (UTC)')
+ logger.debug('Успешно распарсена дата: (UTC)', date_str=date_str, utc_normalized=utc_normalized)
return utc_normalized
except Exception as e:
- logger.warning(f"⚠️ Не удалось распарсить дату '{date_str}': {e}. Используем дефолтную дату.")
+ logger.warning('⚠️ Не удалось распарсить дату . Используем дефолтную дату.', date_str=date_str, error=e)
return self._now_utc() + timedelta(days=30)
def _safe_expire_at_for_panel(self, expire_at: datetime | None) -> datetime:
@@ -299,9 +299,9 @@ class RemnaWaveService:
if normalized_expire < minimum_expire:
logger.debug(
- '⚙️ Коррекция даты истечения (%s) до минимально допустимой (%s) для панели',
- normalized_expire,
- minimum_expire,
+ '⚙️ Коррекция даты истечения до минимально допустимой для панели',
+ normalized_expire=normalized_expire,
+ minimum_expire=minimum_expire,
)
result = minimum_expire
else:
@@ -381,7 +381,7 @@ class RemnaWaveService:
Returns:
Tuple[first_name, last_name, username] - извлеченные данные
"""
- logger.debug(f"📥 Парсинг описания пользователя: '{description}'")
+ logger.debug('📥 Парсинг описания пользователя', description=description)
if not description:
logger.debug('❌ Пустое описание пользователя')
@@ -402,7 +402,7 @@ class RemnaWaveService:
match = re.search(pattern, description)
if match:
user_info = match.group(1).strip()
- logger.debug(f"🔍 Найдена информация о пользователе: '{user_info}'")
+ logger.debug('🔍 Найдена информация о пользователе', user_info=user_info)
break
if not user_info:
@@ -419,29 +419,32 @@ class RemnaWaveService:
# Убираем username из основной информации
name_part = user_info[: username_match.start()].strip()
logger.debug(
- f"📱 Найден username: '{username_with_at}' (обработанный: '{username}'), остаток: '{name_part}'"
+ '📱 Найден username: (обработанный: ), остаток',
+ username_with_at=username_with_at,
+ username=username,
+ name_part=name_part,
)
else:
username = None
name_part = user_info
- logger.debug(f"📱 Username не найден, имя: '{name_part}'")
+ logger.debug('📱 Username не найден, имя', name_part=name_part)
# Разделяем имя и фамилию
if name_part and not name_part.startswith('@'):
# Если есть имя (не начинается с @), используем его
name_parts = name_part.split()
- logger.debug(f'🔤 Части имени: {name_parts}')
+ logger.debug('🔤 Части имени', name_parts=name_parts)
if len(name_parts) >= 2:
# Первое слово - имя, остальные - фамилия
first_name = name_parts[0]
last_name = ' '.join(name_parts[1:]) if len(name_parts) > 1 else None
- logger.debug(f"👤 Имя: '{first_name}', Фамилия: '{last_name}'")
+ logger.debug('👤 Имя: Фамилия', first_name=first_name, last_name=last_name)
elif len(name_parts) == 1 and not name_parts[0].startswith('@'):
# Только имя
first_name = name_parts[0]
last_name = None
- logger.debug(f"👤 Только имя: '{first_name}'")
+ logger.debug('👤 Только имя', first_name=first_name)
else:
first_name = None
last_name = None
@@ -452,7 +455,10 @@ class RemnaWaveService:
logger.debug('👤 Имя не определено (начинается с @)')
logger.debug(
- f"✅ Результат парсинга: first_name='{first_name}', last_name='{last_name}', username='{username}'"
+ '✅ Результат парсинга: first_name=, last_name=, username',
+ first_name=first_name,
+ last_name=last_name,
+ username=username,
)
return first_name, last_name, username
@@ -502,8 +508,7 @@ class RemnaWaveService:
return db_user, True
except IntegrityError as create_error:
logger.info(
- '♻️ Пользователь с telegram_id %s уже существует. Используем существующую запись.',
- telegram_id,
+ '♻️ Пользователь с telegram_id уже существует. Используем существующую запись.', telegram_id=telegram_id
)
try:
@@ -515,20 +520,26 @@ class RemnaWaveService:
try:
existing_user = await get_user_by_telegram_id(db, telegram_id)
if existing_user is None:
- logger.error('❌ Не удалось найти существующего пользователя с telegram_id %s', telegram_id)
+ logger.error(
+ '❌ Не удалось найти существующего пользователя с telegram_id', telegram_id=telegram_id
+ )
return None, False
logger.debug(
- 'Используется существующий пользователь %s после конфликта уникальности: %s',
- telegram_id,
- create_error,
+ 'Используется существующий пользователь после конфликта уникальности',
+ telegram_id=telegram_id,
+ create_error=create_error,
)
return existing_user, False
except Exception as load_error:
- logger.error('❌ Ошибка загрузки существующего пользователя %s: %s', telegram_id, load_error)
+ logger.error(
+ '❌ Ошибка загрузки существующего пользователя', telegram_id=telegram_id, load_error=load_error
+ )
return None, False
except Exception as general_error:
- logger.error('❌ Общая ошибка создания/загрузки пользователя %s: %s', telegram_id, general_error)
+ logger.error(
+ '❌ Общая ошибка создания/загрузки пользователя', telegram_id=telegram_id, general_error=general_error
+ )
try:
await db.rollback()
except:
@@ -544,27 +555,27 @@ class RemnaWaveService:
system_stats = await api.get_system_stats()
logger.info('Системная статистика получена')
except Exception as e:
- logger.error(f'Ошибка получения системной статистики: {e}')
+ logger.error('Ошибка получения системной статистики', error=e)
system_stats = {}
try:
bandwidth_stats = await api.get_bandwidth_stats()
logger.info('Статистика трафика получена')
except Exception as e:
- logger.error(f'Ошибка получения статистики трафика: {e}')
+ logger.error('Ошибка получения статистики трафика', error=e)
bandwidth_stats = {}
try:
realtime_usage = await api.get_nodes_realtime_usage()
logger.info('Реалтайм статистика получена')
except Exception as e:
- logger.error(f'Ошибка получения реалтайм статистики: {e}')
+ logger.error('Ошибка получения реалтайм статистики', error=e)
realtime_usage = []
try:
nodes_stats = await api.get_nodes_statistics()
except Exception as e:
- logger.error(f'Ошибка получения статистики нод: {e}')
+ logger.error('Ошибка получения статистики нод', error=e)
nodes_stats = {}
total_download = sum(node.get('downloadBytes', 0) for node in realtime_usage)
@@ -593,7 +604,7 @@ class RemnaWaveService:
try:
uptime_seconds = int(float(uptime_value)) if uptime_value is not None else 0
except (TypeError, ValueError):
- logger.warning(f"Не удалось преобразовать uptime '{uptime_value}' в число, используем 0")
+ logger.warning('Не удалось преобразовать uptime в число, используем 0', uptime_value=uptime_value)
result = {
'system': {
@@ -674,15 +685,17 @@ class RemnaWaveService:
}
logger.info(
- f'Статистика сформирована: пользователи={result["system"]["total_users"]}, общий трафик={total_user_traffic}'
+ 'Статистика сформирована: пользователи=, общий трафик',
+ result=result['system']['total_users'],
+ total_user_traffic=total_user_traffic,
)
return result
except RemnaWaveAPIError as e:
- logger.error(f'Ошибка Remnawave API при получении статистики: {e}')
+ logger.error('Ошибка Remnawave API при получении статистики', error=e)
return {'error': str(e)}
except Exception as e:
- logger.error(f'Общая ошибка получения системной статистики: {e}')
+ logger.error('Общая ошибка получения системной статистики', error=e)
return {'error': f'Внутренняя ошибка сервера: {e!s}'}
def _parse_bandwidth_string(self, bandwidth_str: str) -> int:
@@ -718,15 +731,15 @@ class RemnaWaveService:
if unit in units:
result = int(value * units[unit])
- logger.debug(f"Парсинг '{bandwidth_str}': {value} {unit} = {result} байт")
+ logger.debug('Парсинг = байт', bandwidth_str=bandwidth_str, value=value, unit=unit, result=result)
return result
- logger.warning(f'Неизвестная единица измерения: {unit}')
+ logger.warning('Неизвестная единица измерения', unit=unit)
- logger.warning(f"Не удалось распарсить строку трафика: '{bandwidth_str}'")
+ logger.warning('Не удалось распарсить строку трафика', bandwidth_str=bandwidth_str)
return 0
except Exception as e:
- logger.error(f"Ошибка парсинга строки трафика '{bandwidth_str}': {e}")
+ logger.error('Ошибка парсинга строки трафика', bandwidth_str=bandwidth_str, error=e)
return 0
async def get_all_nodes(self) -> list[dict[str, Any]]:
@@ -752,11 +765,11 @@ class RemnaWaveService:
}
)
- logger.info(f'✅ Получено {len(result)} нод из Remnawave')
+ logger.info('✅ Получено нод из Remnawave', result_count=len(result))
return result
except Exception as e:
- logger.error(f'Ошибка получения нод из Remnawave: {e}')
+ logger.error('Ошибка получения нод из Remnawave', error=e)
return []
async def test_connection(self) -> bool:
@@ -767,7 +780,7 @@ class RemnaWaveService:
return True
except Exception as e:
- logger.error(f'❌ Ошибка соединения с Remnawave API: {e}')
+ logger.error('❌ Ошибка соединения с Remnawave API', error=e)
return False
async def get_node_details(self, node_uuid: str) -> dict[str, Any] | None:
@@ -806,7 +819,7 @@ class RemnaWaveService:
}
except Exception as e:
- logger.error(f'Ошибка получения информации о ноде {node_uuid}: {e}')
+ logger.error('Ошибка получения информации о ноде', node_uuid=node_uuid, error=e)
return None
async def manage_node(self, node_uuid: str, action: str) -> bool:
@@ -821,11 +834,11 @@ class RemnaWaveService:
else:
return False
- logger.info(f'✅ Действие {action} выполнено для ноды {node_uuid}')
+ logger.info('✅ Действие выполнено для ноды', action=action, node_uuid=node_uuid)
return True
except Exception as e:
- logger.error(f'Ошибка управления нодой {node_uuid}: {e}')
+ logger.error('Ошибка управления нодой', node_uuid=node_uuid, error=e)
return False
async def restart_all_nodes(self) -> bool:
@@ -839,7 +852,7 @@ class RemnaWaveService:
return result
except Exception as e:
- logger.error(f'Ошибка перезагрузки всех нод: {e}')
+ logger.error('Ошибка перезагрузки всех нод', error=e)
return False
async def update_squad_inbounds(self, squad_uuid: str, inbound_uuids: list[str]) -> bool:
@@ -849,7 +862,7 @@ class RemnaWaveService:
await api._make_request('PATCH', '/api/internal-squads', data)
return True
except Exception as e:
- logger.error(f'Error updating squad inbounds: {e}')
+ logger.error('Error updating squad inbounds', error=e)
return False
async def get_all_squads(self) -> list[dict[str, Any]]:
@@ -872,11 +885,11 @@ class RemnaWaveService:
}
)
- logger.info(f'✅ Получено {len(result)} сквадов из Remnawave')
+ logger.info('✅ Получено сквадов из Remnawave', result_count=len(result))
return result
except Exception as e:
- logger.error(f'Ошибка получения сквадов из Remnawave: {e}')
+ logger.error('Ошибка получения сквадов из Remnawave', error=e)
return []
async def create_squad(self, name: str, inbounds: list[str]) -> str | None:
@@ -884,11 +897,11 @@ class RemnaWaveService:
async with self.get_api_client() as api:
squad = await api.create_internal_squad(name, inbounds)
- logger.info(f'✅ Создан новый сквад: {name}')
+ logger.info('✅ Создан новый сквад', name=name)
return squad.uuid
except Exception as e:
- logger.error(f'Ошибка создания сквада {name}: {e}')
+ logger.error('Ошибка создания сквада', name=name, error=e)
return None
async def update_squad(self, uuid: str, name: str = None, inbounds: list[str] = None) -> bool:
@@ -896,11 +909,11 @@ class RemnaWaveService:
async with self.get_api_client() as api:
await api.update_internal_squad(uuid, name, inbounds)
- logger.info(f'✅ Обновлен сквад {uuid}')
+ logger.info('✅ Обновлен сквад', uuid=uuid)
return True
except Exception as e:
- logger.error(f'Ошибка обновления сквада {uuid}: {e}')
+ logger.error('Ошибка обновления сквада', uuid=uuid, error=e)
return False
async def delete_squad(self, uuid: str) -> bool:
@@ -909,12 +922,12 @@ class RemnaWaveService:
result = await api.delete_internal_squad(uuid)
if result:
- logger.info(f'✅ Удален сквад {uuid}')
+ logger.info('✅ Удален сквад', uuid=uuid)
return result
except Exception as e:
- logger.error(f'Ошибка удаления сквада {uuid}: {e}')
+ logger.error('Ошибка удаления сквада', uuid=uuid, error=e)
return False
async def migrate_squad_users(
@@ -965,9 +978,7 @@ class RemnaWaveService:
total_candidates = len(subscriptions)
if not subscriptions:
logger.info(
- '🚚 Переезд сквада %s → %s: подходящих подписок не найдено',
- source_uuid,
- target_uuid,
+ '🚚 Переезд сквада → : подходящих подписок не найдено', source_uuid=source_uuid, target_uuid=target_uuid
)
return {
'success': True,
@@ -1007,8 +1018,8 @@ class RemnaWaveService:
if api is None:
panel_failed += 1
logger.error(
- '❌ RemnaWave API недоступен для обновления пользователя %s',
- subscription.user.telegram_id,
+ '❌ RemnaWave API недоступен для обновления пользователя',
+ telegram_id=subscription.user.telegram_id,
)
continue
@@ -1021,9 +1032,9 @@ class RemnaWaveService:
except Exception as error:
panel_failed += 1
logger.error(
- '❌ Ошибка обновления сквадов пользователя %s: %s',
- subscription.user.telegram_id,
- error,
+ '❌ Ошибка обновления сквадов пользователя',
+ telegram_id=subscription.user.telegram_id,
+ error=error,
)
continue
@@ -1086,11 +1097,11 @@ class RemnaWaveService:
await db.rollback()
logger.info(
- '🚚 Завершен переезд сквада %s → %s: обновлено %s подписок (%s не обновлены в панели)',
- source_uuid,
- target_uuid,
- updated_subscriptions,
- panel_failed,
+ '🚚 Завершен переезд сквада → : обновлено подписок (не обновлены в панели)',
+ source_uuid=source_uuid,
+ target_uuid=target_uuid,
+ updated_subscriptions=updated_subscriptions,
+ panel_failed=panel_failed,
)
return {
@@ -1108,12 +1119,7 @@ class RemnaWaveService:
raise
except Exception as error:
await db.rollback()
- logger.error(
- '❌ Ошибка переезда сквада %s → %s: %s',
- source_uuid,
- target_uuid,
- error,
- )
+ logger.error('❌ Ошибка переезда сквада →', source_uuid=source_uuid, target_uuid=target_uuid, error=error)
return {
'success': False,
'error': 'unexpected',
@@ -1126,7 +1132,7 @@ class RemnaWaveService:
try:
stats = {'created': 0, 'updated': 0, 'errors': 0, 'deleted': 0}
- logger.info(f'🔄 Начинаем синхронизацию типа: {sync_type}')
+ logger.info('🔄 Начинаем синхронизацию типа', sync_type=sync_type)
async with self.get_api_client() as api:
panel_users = []
@@ -1134,7 +1140,7 @@ class RemnaWaveService:
size = 500 # Увеличен размер батча для ускорения загрузки
while True:
- logger.info(f'📥 Загружаем пользователей: start={start}, size={size}')
+ logger.info('📥 Загружаем пользователей: start=, size', start=start, size=size)
# enrich_happ_links=False - happ_crypto_link уже возвращается API в поле happ.cryptoLink
# Не делаем дополнительные HTTP-запросы для каждого пользователя
@@ -1142,7 +1148,9 @@ class RemnaWaveService:
users_batch = response['users']
total_users = response['total']
- logger.info(f'📊 Получено {len(users_batch)} пользователей из {total_users}')
+ logger.info(
+ '📊 Получено пользователей из', users_batch_count=len(users_batch), total_users=total_users
+ )
for user_obj in users_batch:
user_dict = {
@@ -1170,7 +1178,7 @@ class RemnaWaveService:
if start > total_users:
break
- logger.info(f'✅ Всего загружено пользователей из панели: {len(panel_users)}')
+ logger.info('✅ Всего загружено пользователей из панели', panel_users_count=len(panel_users))
# Загрузка пользователей с их подписками за один запрос (bulk loading)
from sqlalchemy import select
@@ -1191,13 +1199,13 @@ class RemnaWaveService:
# Also index email-only users by their remnawave_uuid for sync
email_users_count = sum(1 for u in bot_users if u.telegram_id is None)
if email_users_count > 0:
- logger.info(f'📧 Email-only пользователей (без telegram_id): {email_users_count}')
+ logger.info('📧 Email-only пользователей (без telegram_id)', email_users_count=email_users_count)
- logger.info(f'📊 Пользователей в боте: {len(bot_users)}')
+ logger.info('📊 Пользователей в боте', bot_users_count=len(bot_users))
panel_users_with_tg = [user for user in panel_users if user.get('telegramId') is not None]
- logger.info(f'📊 Пользователей в панели с Telegram ID: {len(panel_users_with_tg)}')
+ logger.info('📊 Пользователей в панели с Telegram ID', panel_users_with_tg_count=len(panel_users_with_tg))
unique_panel_users_map = self._deduplicate_panel_users_by_telegram_id(panel_users_with_tg)
unique_panel_users = list(unique_panel_users_map.values())
@@ -1205,8 +1213,8 @@ class RemnaWaveService:
if duplicates_count:
logger.info(
- '♻️ Обнаружено %s дубликатов пользователей по Telegram ID. Используем самые свежие записи.',
- duplicates_count,
+ '♻️ Обнаружено дубликатов пользователей по Telegram ID. Используем самые свежие записи.',
+ duplicates_count=duplicates_count,
)
panel_telegram_ids = set(unique_panel_users_map.keys())
@@ -1216,7 +1224,10 @@ class RemnaWaveService:
user for user in panel_users if user.get('telegramId') is None and user.get('email')
]
if panel_users_email_only:
- logger.info(f'📧 Пользователей в панели с Email (без Telegram): {len(panel_users_email_only)}')
+ logger.info(
+ '📧 Пользователей в панели с Email (без Telegram)',
+ panel_users_email_only_count=len(panel_users_email_only),
+ )
# Для ускорения - подготовим данные о подписках
# Соберем все существующие подписки за один запрос
@@ -1240,20 +1251,25 @@ class RemnaWaveService:
continue
if (i + 1) % 10 == 0:
- logger.info(f'🔄 Обрабатываем пользователя {i + 1}/{len(unique_panel_users)}: {telegram_id}')
+ logger.info(
+ '🔄 Обрабатываем пользователя /',
+ i=i + 1,
+ unique_panel_users_count=len(unique_panel_users),
+ telegram_id=telegram_id,
+ )
db_user = bot_users_by_telegram_id.get(telegram_id)
if not db_user:
if sync_type in ['new_only', 'all']:
- logger.info(f'🆕 Создание пользователя для telegram_id {telegram_id}')
+ logger.info('🆕 Создание пользователя для telegram_id', telegram_id=telegram_id)
db_user, is_created = await self._get_or_create_bot_user_from_panel(db, panel_user)
if not db_user:
logger.error(
- '❌ Не удалось создать или получить пользователя для telegram_id %s',
- telegram_id,
+ '❌ Не удалось создать или получить пользователя для telegram_id',
+ telegram_id=telegram_id,
)
stats['errors'] += 1
continue
@@ -1265,7 +1281,11 @@ class RemnaWaveService:
updated_fields = []
# Если были обновлены другие поля (подписка, статус и т.д.), сохраняем изменения
if updated_fields:
- logger.info(f'🔄 Обновлены поля {updated_fields} для пользователя {telegram_id}')
+ logger.info(
+ '🔄 Обновлены поля для пользователя',
+ updated_fields=updated_fields,
+ telegram_id=telegram_id,
+ )
await db.flush() # Сохраняем изменения без коммита
_, uuid_mutation = self._ensure_user_remnawave_uuid(
@@ -1277,23 +1297,27 @@ class RemnaWaveService:
if is_created:
await self._create_subscription_from_panel_data(db, db_user, panel_user)
stats['created'] += 1
- logger.info(f'✅ Создан пользователь {telegram_id} с подпиской')
+ logger.info('✅ Создан пользователь с подпиской', telegram_id=telegram_id)
else:
# Обновляем данные существующего пользователя
# Но теперь мы уже загрузили подписку с пользователем, нет необходимости перезагружать
await self._update_subscription_from_panel_data(db, db_user, panel_user)
stats['updated'] += 1
- logger.info(f'♻️ Обновлена подписка существующего пользователя {telegram_id}')
+ logger.info('♻️ Обновлена подписка существующего пользователя', telegram_id=telegram_id)
elif sync_type in ['update_only', 'all']:
- logger.debug(f'🔄 Обновление пользователя {telegram_id}')
+ logger.debug('🔄 Обновление пользователя', telegram_id=telegram_id)
# При синхронизации не обновляем имя и username пользователя
# только сохраняем изменения, если были обновлены другие поля (подписка и т.д.)
updated_fields = []
# Если были обновлены другие поля (подписка, статус и т.д.), сохраняем изменения
if updated_fields:
- logger.info(f'🔄 Обновлены поля {updated_fields} для пользователя {telegram_id}')
+ logger.info(
+ '🔄 Обновлены поля для пользователя',
+ updated_fields=updated_fields,
+ telegram_id=telegram_id,
+ )
await db.flush() # Сохраняем изменения без коммита
# Обновляем UUID ДО операций с подпиской, чтобы избежать
@@ -1315,10 +1339,10 @@ class RemnaWaveService:
await self._create_subscription_from_panel_data(db, db_user, panel_user)
stats['updated'] += 1
- logger.debug(f'✅ Обновлён пользователь {telegram_id}')
+ logger.debug('✅ Обновлён пользователь', telegram_id=telegram_id)
except Exception as user_error:
- logger.error(f'❌ Ошибка обработки пользователя {telegram_id}: {user_error}')
+ logger.error('❌ Ошибка обработки пользователя', telegram_id=telegram_id, user_error=user_error)
stats['errors'] += 1
if uuid_mutation:
uuid_mutation.rollback()
@@ -1335,9 +1359,9 @@ class RemnaWaveService:
# in async context (greenlet_spawn error). Break the loop to
# prevent cascading failures for every remaining user.
logger.warning(
- '⚠️ Сессия повреждена после rollback, прерываем обработку (обработано %d/%d пользователей)',
- i + 1,
- len(unique_panel_users),
+ '⚠️ Сессия повреждена после rollback, прерываем обработку (обработано / пользователей)',
+ i=i + 1,
+ unique_panel_users_count=len(unique_panel_users),
)
break
@@ -1349,10 +1373,12 @@ class RemnaWaveService:
if (i + 1) % batch_size == 0:
try:
await db.commit()
- logger.debug(f'📦 Коммит изменений после обработки {i + 1} пользователей')
+ logger.debug('📦 Коммит изменений после обработки пользователей', i=i + 1)
pending_uuid_mutations.clear()
except Exception as commit_error:
- logger.error(f'❌ Ошибка коммита после обработки {i + 1} пользователей: {commit_error}')
+ logger.error(
+ '❌ Ошибка коммита после обработки пользователей', i=i + 1, commit_error=commit_error
+ )
await db.rollback()
for mutation in reversed(pending_uuid_mutations):
mutation.rollback()
@@ -1364,7 +1390,7 @@ class RemnaWaveService:
await db.commit()
pending_uuid_mutations.clear()
except Exception as final_commit_error:
- logger.error(f'❌ Ошибка финального коммита: {final_commit_error}')
+ logger.error('❌ Ошибка финального коммита', final_commit_error=final_commit_error)
await db.rollback()
for mutation in reversed(pending_uuid_mutations):
mutation.rollback()
@@ -1372,7 +1398,10 @@ class RemnaWaveService:
# Обработка email-only пользователей из панели
if panel_users_email_only and sync_type in ['new_only', 'all']:
- logger.info(f'📧 Обработка {len(panel_users_email_only)} email-only пользователей из панели...')
+ logger.info(
+ '📧 Обработка email-only пользователей из панели...',
+ panel_users_email_only_count=len(panel_users_email_only),
+ )
for panel_user in panel_users_email_only:
try:
@@ -1406,20 +1435,20 @@ class RemnaWaveService:
await self._create_subscription_from_panel_data(db, db_user, panel_user)
stats['updated'] += 1
- logger.info(f'📧 Обновлен email-пользователь: {panel_email}')
+ logger.info('📧 Обновлен email-пользователь', panel_email=panel_email)
else:
# Email-only пользователи не создаются автоматически при синхронизации,
# они должны сначала зарегистрироваться через cabinet
- logger.debug(f'📧 Email-пользователь {panel_email} не найден в боте, пропускаем')
+ logger.debug('📧 Email-пользователь не найден в боте, пропускаем', panel_email=panel_email)
except Exception as email_user_error:
- logger.error(f'❌ Ошибка обработки email-пользователя: {email_user_error}')
+ logger.error('❌ Ошибка обработки email-пользователя', email_user_error=email_user_error)
stats['errors'] += 1
try:
await db.commit()
except Exception as email_commit_error:
- logger.error(f'❌ Ошибка коммита email-пользователей: {email_commit_error}')
+ logger.error('❌ Ошибка коммита email-пользователей', email_commit_error=email_commit_error)
await db.rollback()
if sync_type == 'all':
@@ -1439,7 +1468,9 @@ class RemnaWaveService:
]
if users_to_deactivate:
- logger.info(f'📊 Найдено {len(users_to_deactivate)} пользователей для деактивации')
+ logger.info(
+ '📊 Найдено пользователей для деактивации', users_to_deactivate_count=len(users_to_deactivate)
+ )
# Используем один API клиент для всех операций сброса HWID
hwid_api_client = None
@@ -1448,7 +1479,7 @@ class RemnaWaveService:
hwid_api_cm = self.get_api_client()
hwid_api_client = await hwid_api_cm.__aenter__()
except Exception as api_init_error:
- logger.warning(f'⚠️ Не удалось создать API клиент для сброса HWID: {api_init_error}')
+ logger.warning('⚠️ Не удалось создать API клиент для сброса HWID', api_init_error=api_init_error)
hwid_api_client = None
hwid_api_cm = None
@@ -1463,20 +1494,26 @@ class RemnaWaveService:
if subscription and is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск деактивации подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск деактивации подписки : обновлена вебхуком недавно',
+ subscription_id=subscription.id,
)
continue
- logger.info(f'🗑️ Деактивация подписки пользователя {telegram_id} (нет в панели)')
+ logger.info('🗑️ Деактивация подписки пользователя (нет в панели)', telegram_id=telegram_id)
if db_user.remnawave_uuid and hwid_api_client:
try:
devices_reset = await hwid_api_client.reset_user_devices(db_user.remnawave_uuid)
if devices_reset:
- logger.info(f'🔧 Сброшены HWID устройства для пользователя {telegram_id}')
+ logger.info(
+ '🔧 Сброшены HWID устройства для пользователя', telegram_id=telegram_id
+ )
except Exception as hwid_error:
- logger.error(f'❌ Ошибка сброса HWID устройств для {telegram_id}: {hwid_error}')
+ logger.error(
+ '❌ Ошибка сброса HWID устройств для',
+ telegram_id=telegram_id,
+ hwid_error=hwid_error,
+ )
try:
from sqlalchemy import delete
@@ -1490,9 +1527,9 @@ class RemnaWaveService:
SubscriptionServer.subscription_id == subscription.id
)
)
- logger.info(f'🗑️ Удалены серверы подписки для {telegram_id}')
+ logger.info('🗑️ Удалены серверы подписки для', telegram_id=telegram_id)
except Exception as servers_error:
- logger.warning(f'⚠️ Не удалось удалить серверы подписки: {servers_error}')
+ logger.warning('⚠️ Не удалось удалить серверы подписки', servers_error=servers_error)
from app.database.models import SubscriptionStatus
@@ -1505,8 +1542,10 @@ class RemnaWaveService:
# Для платных подписок - НЕ сбрасываем is_trial и end_date!
# Сохраняем оригинальные значения чтобы можно было восстановить
logger.warning(
- f'⚠️ ПЛАТНАЯ подписка пользователя {telegram_id} отключена (нет в панели), '
- f'но is_trial={subscription.is_trial} и end_date={subscription.end_date} СОХРАНЕНЫ'
+ '⚠️ ПЛАТНАЯ подписка пользователя отключена (нет в панели), но is_trial= и end_date= СОХРАНЕНЫ',
+ telegram_id=telegram_id,
+ is_trial=subscription.is_trial,
+ end_date=subscription.end_date,
)
else:
# Для триальных подписок - сбрасываем как раньше
@@ -1530,12 +1569,16 @@ class RemnaWaveService:
cleanup_mutation.set_user_updated_at(db_user, datetime.utcnow())
stats['deleted'] += 1
- logger.info(f'✅ Деактивирована подписка пользователя {telegram_id} (сохранен баланс)')
+ logger.info(
+ '✅ Деактивирована подписка пользователя (сохранен баланс)', telegram_id=telegram_id
+ )
processed_count += 1
except Exception as delete_error:
- logger.error(f'❌ Ошибка деактивации подписки {telegram_id}: {delete_error}')
+ logger.error(
+ '❌ Ошибка деактивации подписки', telegram_id=telegram_id, delete_error=delete_error
+ )
stats['errors'] += 1
if cleanup_mutation:
cleanup_mutation.rollback()
@@ -1555,11 +1598,16 @@ class RemnaWaveService:
if processed_count % batch_size == 0:
try:
await db.commit()
- logger.debug(f'📦 Коммит изменений после деактивации {processed_count} подписок')
+ logger.debug(
+ '📦 Коммит изменений после деактивации подписок',
+ processed_count=processed_count,
+ )
cleanup_uuid_mutations.clear()
except Exception as commit_error:
logger.error(
- f'❌ Ошибка коммита после деактивации {processed_count} подписок: {commit_error}'
+ '❌ Ошибка коммита после деактивации подписок',
+ processed_count=processed_count,
+ commit_error=commit_error,
)
await db.rollback()
for mutation in reversed(cleanup_uuid_mutations):
@@ -1573,7 +1621,9 @@ class RemnaWaveService:
await db.commit()
cleanup_uuid_mutations.clear()
except Exception as final_commit_error:
- logger.error(f'❌ Ошибка финального коммита при деактивации: {final_commit_error}')
+ logger.error(
+ '❌ Ошибка финального коммита при деактивации', final_commit_error=final_commit_error
+ )
await db.rollback()
for mutation in reversed(cleanup_uuid_mutations):
mutation.rollback()
@@ -1588,12 +1638,16 @@ class RemnaWaveService:
pass
logger.info(
- f'🎯 Синхронизация завершена: создано {stats["created"]}, обновлено {stats["updated"]}, деактивировано {stats["deleted"]}, ошибок {stats["errors"]}'
+ '🎯 Синхронизация завершена: создано обновлено деактивировано ошибок',
+ stats=stats['created'],
+ stats_2=stats['updated'],
+ stats_3=stats['deleted'],
+ stats_4=stats['errors'],
)
return stats
except Exception as e:
- logger.error(f'❌ Критическая ошибка синхронизации пользователей: {e}')
+ logger.error('❌ Критическая ошибка синхронизации пользователей', error=e)
return {'created': 0, 'updated': 0, 'errors': 1, 'deleted': 0}
async def _create_subscription_from_panel_data(self, db: AsyncSession, user, panel_user):
@@ -1646,10 +1700,12 @@ class RemnaWaveService:
}
await create_subscription_no_commit(db, **subscription_data)
- logger.info(f'✅ Подготовлена подписка для пользователя {user.telegram_id} до {expire_at}')
+ logger.info(
+ '✅ Подготовлена подписка для пользователя до', telegram_id=user.telegram_id, expire_at=expire_at
+ )
except Exception as e:
- logger.error(f'❌ Ошибка создания подписки для пользователя {user.telegram_id}: {e}')
+ logger.error('❌ Ошибка создания подписки для пользователя', telegram_id=user.telegram_id, error=e)
try:
from app.database.crud.subscription import create_subscription_no_commit
from app.database.models import SubscriptionStatus
@@ -1670,9 +1726,9 @@ class RemnaWaveService:
panel_user.get('subscriptionCryptoLink') or (panel_user.get('happ') or {}).get('cryptoLink', '')
),
)
- logger.info(f'✅ Подготовлена базовая подписка для пользователя {user.telegram_id}')
+ logger.info('✅ Подготовлена базовая подписка для пользователя', telegram_id=user.telegram_id)
except Exception as basic_error:
- logger.error(f'❌ Ошибка создания базовой подписки: {basic_error}')
+ logger.error('❌ Ошибка создания базовой подписки', basic_error=basic_error)
async def _update_subscription_from_panel_data(self, db: AsyncSession, user, panel_user):
try:
@@ -1690,8 +1746,7 @@ class RemnaWaveService:
# Skip if recently updated by webhook (prevent stale data overwrite)
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск синхронизации подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск синхронизации подписки : обновлена вебхуком недавно', subscription_id=subscription.id
)
return
@@ -1724,25 +1779,33 @@ class RemnaWaveService:
.replace(tzinfo=None)
)
logger.info(
- f'✅ Sync: обновлена end_date для user {getattr(user, "telegram_id", "?")}: '
- f'{subscription.end_date} -> {new_end_date_local} (разница: {time_diff:.0f}с)'
+ '✅ Sync: обновлена end_date для user -> (разница: с)',
+ value=getattr(user, 'telegram_id', '?'),
+ end_date=subscription.end_date,
+ new_end_date_local=new_end_date_local,
+ time_diff=round(time_diff, 0),
)
subscription.end_date = new_end_date_local
else:
# Локальная дата позже - НЕ перезаписываем
logger.debug(
- f'⏭️ Sync: end_date для user {getattr(user, "telegram_id", "?")} актуальна: '
- f'локальная ({subscription.end_date} / UTC: {local_end_date_utc}) >= RemnaWave ({expire_at} UTC)'
+ '⏭️ Sync: end_date для user актуальна: локальная ( UTC: ) RemnaWave ( UTC)',
+ value=getattr(user, 'telegram_id', '?'),
+ end_date=subscription.end_date,
+ local_end_date_utc=local_end_date_utc,
+ expire_at=expire_at,
)
else:
logger.debug(
- f'⏭️ Sync: пропускаем обновление end_date для user {getattr(user, "telegram_id", "?")}: '
- f'разница слишком мала ({time_diff:.0f}с < 60с)'
+ '⏭️ Sync: пропускаем обновление end_date для user разница слишком мала (с < 60с)',
+ value=getattr(user, 'telegram_id', '?'),
+ time_diff=round(time_diff, 0),
)
else:
logger.debug(
- f'⏭️ Sync: пропускаем обновление end_date для user {getattr(user, "telegram_id", "?")}: '
- f'панель не ACTIVE (статус: {panel_status})'
+ '⏭️ Sync: пропускаем обновление end_date для user панель не ACTIVE (статус: )',
+ value=getattr(user, 'telegram_id', '?'),
+ panel_status=panel_status,
)
current_time = self._now_utc()
@@ -1759,9 +1822,11 @@ class RemnaWaveService:
# а реальная end_date уже обновлена продлением
if subscription.status == SubscriptionStatus.ACTIVE.value:
logger.warning(
- f'⚠️ Sync: пропускаем деактивацию подписки user {getattr(user, "telegram_id", "?")}: '
- f'статус ACTIVE, end_date ({subscription.end_date} / UTC: {end_date_utc}) <= now ({current_time}). '
- f'Деактивация будет выполнена через middleware с буфером.'
+ '⚠️ Sync: пропускаем деактивацию подписки user статус ACTIVE, end_date ( UTC: ) <= now . Деактивация будет выполнена через middleware с буфером.',
+ value=getattr(user, 'telegram_id', '?'),
+ end_date=subscription.end_date,
+ end_date_utc=end_date_utc,
+ current_time=current_time,
)
new_status = subscription.status # Сохраняем текущий статус
else:
@@ -1771,36 +1836,36 @@ class RemnaWaveService:
if subscription.status != new_status:
subscription.status = new_status
- logger.debug(f'Обновлен статус подписки: {new_status}')
+ logger.debug('Обновлен статус подписки', new_status=new_status)
used_traffic_bytes = _get_user_traffic_bytes(panel_user)
traffic_used_gb = used_traffic_bytes / (1024**3)
if abs(subscription.traffic_used_gb - traffic_used_gb) > 0.01:
subscription.traffic_used_gb = traffic_used_gb
- logger.debug(f'Обновлен использованный трафик: {traffic_used_gb} GB')
+ logger.debug('Обновлен использованный трафик', traffic_used_gb=traffic_used_gb)
traffic_limit_bytes = panel_user.get('trafficLimitBytes', 0)
traffic_limit_gb = traffic_limit_bytes // (1024**3) if traffic_limit_bytes > 0 else 0
if subscription.traffic_limit_gb != traffic_limit_gb:
subscription.traffic_limit_gb = traffic_limit_gb
- logger.debug(f'Обновлен лимит трафика: {traffic_limit_gb} GB')
+ logger.debug('Обновлен лимит трафика', traffic_limit_gb=traffic_limit_gb)
device_limit = panel_user.get('hwidDeviceLimit', 1) or 1
if subscription.device_limit != device_limit:
subscription.device_limit = device_limit
- logger.debug(f'Обновлен лимит устройств: {device_limit}')
+ logger.debug('Обновлен лимит устройств', device_limit=device_limit)
new_short_uuid = panel_user.get('shortUuid')
if new_short_uuid and subscription.remnawave_short_uuid != new_short_uuid:
old_short_uuid = subscription.remnawave_short_uuid
subscription.remnawave_short_uuid = new_short_uuid
logger.debug(
- 'Обновлен short UUID подписки пользователя %s: %s → %s',
- getattr(user, 'telegram_id', '?'),
- old_short_uuid,
- new_short_uuid,
+ 'Обновлен short UUID подписки пользователя : →',
+ getattr=getattr(user, 'telegram_id', '?'),
+ old_short_uuid=old_short_uuid,
+ new_short_uuid=new_short_uuid,
)
panel_url = panel_user.get('subscriptionUrl', '')
@@ -1827,13 +1892,13 @@ class RemnaWaveService:
if current_squads != new_squads:
subscription.connected_squads = squad_uuids
- logger.debug(f'Обновлены подключенные сквады: {squad_uuids}')
+ logger.debug('Обновлены подключенные сквады', squad_uuids=squad_uuids)
# Коммитим изменения позже, в основном цикле, чтобы уменьшить количество транзакций
- logger.debug(f'✅ Обновлена подписка для пользователя {user.telegram_id}')
+ logger.debug('✅ Обновлена подписка для пользователя', telegram_id=user.telegram_id)
except Exception as e:
- logger.error(f'❌ Ошибка обновления подписки для пользователя {user.telegram_id}: {e}')
+ logger.error('❌ Ошибка обновления подписки для пользователя', telegram_id=user.telegram_id, error=e)
# Не делаем rollback, так как это может повлиять на другие операции
# Ошибку прокидываем выше для корректной обработки в основном цикле
raise
@@ -1924,7 +1989,11 @@ class RemnaWaveService:
existing_users = await api.get_user_by_telegram_id(user.telegram_id)
if existing_users:
panel_uuid = existing_users[0].uuid
- logger.debug(f'Найден пользователь {user.telegram_id} в панели: {panel_uuid}')
+ logger.debug(
+ 'Найден пользователь в панели',
+ telegram_id=user.telegram_id,
+ panel_uuid=panel_uuid,
+ )
# Fallback: поиск по email (для OAuth юзеров без telegram_id)
if not panel_uuid and user.email:
@@ -1932,7 +2001,9 @@ class RemnaWaveService:
if existing_users:
panel_uuid = existing_users[0].uuid
logger.debug(
- f'Найден пользователь {user.email} в панели по email: {panel_uuid}'
+ 'Найден пользователь в панели по email',
+ email=user.email,
+ panel_uuid=panel_uuid,
)
if panel_uuid:
@@ -1967,7 +2038,9 @@ class RemnaWaveService:
except Exception as e:
logger.error(
- f'Ошибка синхронизации пользователя {sub.user.telegram_id if sub.user else "N/A"} в панель: {e}'
+ 'Ошибка синхронизации пользователя в панель',
+ telegram_id=sub.user.telegram_id if sub.user else 'N/A',
+ error=e,
)
return ('error', sub, None)
@@ -1995,16 +2068,16 @@ class RemnaWaveService:
try:
await db.commit()
except Exception as commit_error:
- logger.error(
- 'Ошибка фиксации транзакции при синхронизации в панель: %s',
- commit_error,
- )
+ logger.error('Ошибка фиксации транзакции при синхронизации в панель', commit_error=commit_error)
await db.rollback()
stats['errors'] += len(valid_subscriptions)
logger.info(
- f'📦 Обработано {offset + len(subscriptions)} подписок: '
- f'создано {stats["created"]}, обновлено {stats["updated"]}, ошибок {stats["errors"]}'
+ '📦 Обработано подписок: создано обновлено ошибок',
+ offset=offset + len(subscriptions),
+ stats=stats['created'],
+ stats_2=stats['updated'],
+ stats_3=stats['errors'],
)
if len(subscriptions) < batch_size:
@@ -2013,12 +2086,15 @@ class RemnaWaveService:
offset += batch_size
logger.info(
- f'✅ Синхронизация в панель завершена: создано {stats["created"]}, обновлено {stats["updated"]}, ошибок {stats["errors"]}'
+ '✅ Синхронизация в панель завершена: создано обновлено ошибок',
+ stats=stats['created'],
+ stats_2=stats['updated'],
+ stats_3=stats['errors'],
)
return stats
except Exception as e:
- logger.error(f'Ошибка синхронизации пользователей в панель: {e}')
+ logger.error('Ошибка синхронизации пользователей в панель', error=e)
return {'created': 0, 'updated': 0, 'errors': 1}
async def get_user_traffic_stats(self, telegram_id: int) -> dict[str, Any] | None:
@@ -2042,7 +2118,7 @@ class RemnaWaveService:
}
except Exception as e:
- logger.error(f'Ошибка получения статистики трафика для пользователя {telegram_id}: {e}')
+ logger.error('Ошибка получения статистики трафика для пользователя', telegram_id=telegram_id, error=e)
return None
async def get_user_traffic_stats_by_uuid(self, remnawave_uuid: str) -> dict[str, Any] | None:
@@ -2069,7 +2145,7 @@ class RemnaWaveService:
}
except Exception as e:
- logger.error(f'Ошибка получения статистики трафика по UUID {remnawave_uuid}: {e}')
+ logger.error('Ошибка получения статистики трафика по UUID', remnawave_uuid=remnawave_uuid, error=e)
return None
async def get_telegram_id_by_email(self, user_identifier: str) -> int | None:
@@ -2093,11 +2169,13 @@ class RemnaWaveService:
user = await api.get_user_by_username(user_identifier)
if user and user.telegram_id:
logger.info(
- f"Найден пользователь по username '{user_identifier}': telegram_id={user.telegram_id}"
+ 'Найден пользователь по username telegram_id',
+ user_identifier=user_identifier,
+ telegram_id=user.telegram_id,
)
return user.telegram_id
except Exception as e:
- logger.debug(f"Пользователь не найден по username '{user_identifier}': {e}")
+ logger.debug('Пользователь не найден по username', user_identifier=user_identifier, error=e)
# Если не нашли по username, ищем по email среди всех пользователей
try:
@@ -2110,17 +2188,19 @@ class RemnaWaveService:
panel_telegram_id = panel_user.telegram_id if hasattr(panel_user, 'telegram_id') else None
if panel_telegram_id:
logger.info(
- f"Найден пользователь по email '{user_identifier}': telegram_id={panel_telegram_id}"
+ 'Найден пользователь по email telegram_id',
+ user_identifier=user_identifier,
+ panel_telegram_id=panel_telegram_id,
)
return panel_telegram_id
except Exception as e:
- logger.warning(f"Ошибка поиска пользователя по email '{user_identifier}': {e}")
+ logger.warning('Ошибка поиска пользователя по email', user_identifier=user_identifier, error=e)
- logger.warning(f"Пользователь с идентификатором '{user_identifier}' не найден в панели")
+ logger.warning('Пользователь с идентификатором не найден в панели', user_identifier=user_identifier)
return None
except Exception as e:
- logger.error(f"Ошибка получения telegram_id для '{user_identifier}': {e}")
+ logger.error('Ошибка получения telegram_id для', user_identifier=user_identifier, error=e)
return None
async def test_api_connection(self) -> dict[str, Any]:
@@ -2164,7 +2244,7 @@ class RemnaWaveService:
return usage_data
except Exception as e:
- logger.error(f'Ошибка получения актуального использования нод: {e}')
+ logger.error('Ошибка получения актуального использования нод', error=e)
return []
async def get_squad_details(self, squad_uuid: str) -> dict | None:
@@ -2184,7 +2264,7 @@ class RemnaWaveService:
}
return None
except Exception as e:
- logger.error(f'Error getting squad details: {e}')
+ logger.error('Error getting squad details', error=e)
return None
async def add_all_users_to_squad(self, squad_uuid: str) -> bool:
@@ -2193,7 +2273,7 @@ class RemnaWaveService:
response = await api._make_request('POST', f'/api/internal-squads/{squad_uuid}/bulk-actions/add-users')
return response.get('response', {}).get('eventSent', False)
except Exception as e:
- logger.error(f'Error adding users to squad: {e}')
+ logger.error('Error adding users to squad', error=e)
return False
async def remove_all_users_from_squad(self, squad_uuid: str) -> bool:
@@ -2204,7 +2284,7 @@ class RemnaWaveService:
)
return response.get('response', {}).get('eventSent', False)
except Exception as e:
- logger.error(f'Error removing users from squad: {e}')
+ logger.error('Error removing users from squad', error=e)
return False
async def get_all_inbounds(self) -> list[dict]:
@@ -2225,7 +2305,7 @@ class RemnaWaveService:
for inbound in inbounds_data
]
except Exception as e:
- logger.error(f'Error getting all inbounds: {e}')
+ logger.error('Error getting all inbounds', error=e)
return []
async def rename_squad(self, squad_uuid: str, new_name: str) -> bool:
@@ -2235,7 +2315,7 @@ class RemnaWaveService:
await api._make_request('PATCH', '/api/internal-squads', data)
return True
except Exception as e:
- logger.error(f'Error renaming squad: {e}')
+ logger.error('Error renaming squad', error=e)
return False
async def get_node_user_usage_by_range(self, node_uuid: str, start_date, end_date) -> list[dict[str, Any]]:
@@ -2253,7 +2333,7 @@ class RemnaWaveService:
return usage_data.get('response', [])
except Exception as e:
- logger.error(f'Ошибка получения статистики использования ноды {node_uuid}: {e}')
+ logger.error('Ошибка получения статистики использования ноды', node_uuid=node_uuid, error=e)
return []
async def get_node_statistics(self, node_uuid: str) -> dict[str, Any] | None:
@@ -2283,26 +2363,26 @@ class RemnaWaveService:
}
except Exception as e:
- logger.error(f'Ошибка получения статистики ноды {node_uuid}: {e}')
+ logger.error('Ошибка получения статистики ноды', node_uuid=node_uuid, error=e)
async def validate_user_data_before_sync(self, panel_user) -> bool:
try:
if not panel_user.telegram_id:
- logger.debug(f'Нет telegram_id для пользователя {panel_user.uuid}')
+ logger.debug('Нет telegram_id для пользователя', uuid=panel_user.uuid)
return False
if not panel_user.uuid:
- logger.debug(f'Нет UUID для пользователя {panel_user.telegram_id}')
+ logger.debug('Нет UUID для пользователя', telegram_id=panel_user.telegram_id)
return False
if panel_user.telegram_id <= 0:
- logger.debug(f'Некорректный telegram_id: {panel_user.telegram_id}')
+ logger.debug('Некорректный telegram_id', telegram_id=panel_user.telegram_id)
return False
return True
except Exception as e:
- logger.error(f'Ошибка валидации данных пользователя: {e}')
+ logger.error('Ошибка валидации данных пользователя', error=e)
return False
async def force_cleanup_user_data(self, db: AsyncSession, user: User) -> bool:
@@ -2320,22 +2400,23 @@ class RemnaWaveService:
user_id_display = user.telegram_id or user.email or f'#{user.id}'
if was_paid:
logger.warning(
- f'⚠️ ВНИМАНИЕ: force_cleanup_user_data вызвана для ПЛАТНОГО пользователя {user_id_display}! '
- f'has_had_paid_subscription={user.has_had_paid_subscription}, '
- f'balance={user.balance_kopeks}, '
- f'is_trial={user.subscription.is_trial if user.subscription else "N/A"}'
+ '⚠️ ВНИМАНИЕ: force_cleanup_user_data вызвана для ПЛАТНОГО пользователя ! has_had_paid_subscription=, balance=, is_trial',
+ user_id_display=user_id_display,
+ has_had_paid_subscription=user.has_had_paid_subscription,
+ balance_kopeks=user.balance_kopeks,
+ is_trial=user.subscription.is_trial if user.subscription else 'N/A',
)
- logger.info(f'🗑️ ПРИНУДИТЕЛЬНАЯ полная очистка данных пользователя {user_id_display}')
+ logger.info('🗑️ ПРИНУДИТЕЛЬНАЯ полная очистка данных пользователя', user_id_display=user_id_display)
if user.remnawave_uuid:
try:
async with self.get_api_client() as api:
devices_reset = await api.reset_user_devices(user.remnawave_uuid)
if devices_reset:
- logger.info(f'🔧 Сброшены HWID устройства для {user_id_display}')
+ logger.info('🔧 Сброшены HWID устройства для', user_id_display=user_id_display)
except Exception as hwid_error:
- logger.warning(f'⚠️ Ошибка сброса HWID устройств: {hwid_error}')
+ logger.warning('⚠️ Ошибка сброса HWID устройств', hwid_error=hwid_error)
try:
from sqlalchemy import delete
@@ -2354,20 +2435,20 @@ class RemnaWaveService:
await db.execute(
delete(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
)
- logger.info(f'🗑️ Удалены серверы подписки для {user_id_display}')
+ logger.info('🗑️ Удалены серверы подписки для', user_id_display=user_id_display)
await db.execute(delete(Transaction).where(Transaction.user_id == user.id))
- logger.info(f'🗑️ Удалены транзакции для {user_id_display}')
+ logger.info('🗑️ Удалены транзакции для', user_id_display=user_id_display)
await db.execute(delete(ReferralEarning).where(ReferralEarning.user_id == user.id))
await db.execute(delete(ReferralEarning).where(ReferralEarning.referral_id == user.id))
- logger.info(f'🗑️ Удалены реферальные доходы для {user_id_display}')
+ logger.info('🗑️ Удалены реферальные доходы для', user_id_display=user_id_display)
await db.execute(delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id))
- logger.info(f'🗑️ Удалены использования промокодов для {user_id_display}')
+ logger.info('🗑️ Удалены использования промокодов для', user_id_display=user_id_display)
except Exception as records_error:
- logger.error(f'❌ Ошибка удаления связанных записей: {records_error}')
+ logger.error('❌ Ошибка удаления связанных записей', records_error=records_error)
try:
user.balance_kopeks = 0
@@ -2393,16 +2474,18 @@ class RemnaWaveService:
await db.commit()
- logger.info(f'✅ ПРИНУДИТЕЛЬНО очищены ВСЕ данные пользователя {user_id_display}')
+ logger.info('✅ ПРИНУДИТЕЛЬНО очищены ВСЕ данные пользователя', user_id_display=user_id_display)
return True
except Exception as cleanup_error:
- logger.error(f'❌ Ошибка финальной очистки пользователя: {cleanup_error}')
+ logger.error('❌ Ошибка финальной очистки пользователя', cleanup_error=cleanup_error)
await db.rollback()
return False
except Exception as e:
- logger.error(f'❌ Критическая ошибка принудительной очистки пользователя {user.telegram_id}: {e}')
+ logger.error(
+ '❌ Критическая ошибка принудительной очистки пользователя', telegram_id=user.telegram_id, error=e
+ )
await db.rollback()
return False
@@ -2422,7 +2505,7 @@ class RemnaWaveService:
if telegram_id:
panel_telegram_ids.add(telegram_id)
- logger.info(f'📊 Найдено {len(panel_telegram_ids)} пользователей в панели')
+ logger.info('📊 Найдено пользователей в панели', panel_telegram_ids_count=len(panel_telegram_ids))
from app.database.crud.subscription import get_all_subscriptions
from app.database.models import SubscriptionStatus
@@ -2446,7 +2529,8 @@ class RemnaWaveService:
if user.telegram_id not in panel_telegram_ids:
logger.info(
- f'🗑️ ПОЛНАЯ деактивация подписки пользователя {user.telegram_id} (отсутствует в панели)'
+ '🗑️ ПОЛНАЯ деактивация подписки пользователя (отсутствует в панели)',
+ telegram_id=user.telegram_id,
)
cleanup_success = await self.force_cleanup_user_data(db, user)
@@ -2457,7 +2541,9 @@ class RemnaWaveService:
stats['errors'] += 1
except Exception as sub_error:
- logger.error(f'❌ Ошибка обработки подписки {subscription.id}: {sub_error}')
+ logger.error(
+ '❌ Ошибка обработки подписки', subscription_id=subscription.id, sub_error=sub_error
+ )
stats['errors'] += 1
page += 1
@@ -2465,12 +2551,15 @@ class RemnaWaveService:
break
logger.info(
- f'🧹 Усиленная очистка завершена: проверено {stats["checked"]}, деактивировано {stats["deactivated"]}, ошибок {stats["errors"]}'
+ '🧹 Усиленная очистка завершена: проверено деактивировано ошибок',
+ stats=stats['checked'],
+ stats_2=stats['deactivated'],
+ stats_3=stats['errors'],
)
return stats
except Exception as e:
- logger.error(f'❌ Критическая ошибка усиленной очистки подписок: {e}')
+ logger.error('❌ Критическая ошибка усиленной очистки подписок', error=e)
return {'deactivated': 0, 'errors': 1, 'checked': 0}
async def sync_subscription_statuses(self, db: AsyncSession) -> dict[str, int]:
@@ -2489,7 +2578,9 @@ class RemnaWaveService:
if telegram_id:
panel_users_dict[telegram_id] = panel_user
- logger.info(f'📊 Найдено {len(panel_users_dict)} пользователей в панели для синхронизации')
+ logger.info(
+ '📊 Найдено пользователей в панели для синхронизации', panel_users_dict_count=len(panel_users_dict)
+ )
from app.database.crud.subscription import get_all_subscriptions
from app.database.models import SubscriptionStatus
@@ -2510,7 +2601,7 @@ class RemnaWaveService:
# Skip email-only users (no telegram_id for panel lookup)
if not user.telegram_id:
- logger.debug(f'Пропускаем email-пользователя {user.id} при синхронизации с панелью')
+ logger.debug('Пропускаем email-пользователя при синхронизации с панелью', user_id=user.id)
continue
panel_user = panel_users_dict.get(user.telegram_id)
@@ -2526,16 +2617,20 @@ class RemnaWaveService:
if is_recently_updated_by_webhook(subscription):
logger.debug(
- 'Пропуск деактивации подписки %s: обновлена вебхуком недавно',
- subscription.id,
+ 'Пропуск деактивации подписки : обновлена вебхуком недавно',
+ subscription_id=subscription.id,
)
else:
- logger.info(f'🗑️ Деактивируем подписку пользователя {user.telegram_id} (нет в панели)')
+ logger.info(
+ '🗑️ Деактивируем подписку пользователя (нет в панели)', telegram_id=user.telegram_id
+ )
await deactivate_subscription(db, subscription)
stats['updated'] += 1
except Exception as sub_error:
- logger.error(f'❌ Ошибка синхронизации подписки {subscription.id}: {sub_error}')
+ logger.error(
+ '❌ Ошибка синхронизации подписки', subscription_id=subscription.id, sub_error=sub_error
+ )
stats['errors'] += 1
page += 1
@@ -2543,12 +2638,15 @@ class RemnaWaveService:
break
logger.info(
- f'🔄 Синхронизация статусов завершена: проверено {stats["checked"]}, обновлено {stats["updated"]}, ошибок {stats["errors"]}'
+ '🔄 Синхронизация статусов завершена: проверено обновлено ошибок',
+ stats=stats['checked'],
+ stats_2=stats['updated'],
+ stats_3=stats['errors'],
)
return stats
except Exception as e:
- logger.error(f'❌ Критическая ошибка синхронизации статусов: {e}')
+ logger.error('❌ Критическая ошибка синхронизации статусов', error=e)
return {'updated': 0, 'errors': 1, 'checked': 0}
async def validate_and_fix_subscriptions(self, db: AsyncSession) -> dict[str, int]:
@@ -2589,8 +2687,10 @@ class RemnaWaveService:
):
time_since_expiry = current_time - end_date_utc
logger.warning(
- f'🔧 fix_data_issues: деактивируем подписку {subscription.id} '
- f'(user={user.telegram_id}), просрочена на {time_since_expiry}'
+ '🔧 fix_data_issues: деактивируем подписку (user=), просрочена на',
+ subscription_id=subscription.id,
+ telegram_id=user.telegram_id,
+ time_since_expiry=time_since_expiry,
)
subscription.status = SubscriptionStatus.EXPIRED.value
issues_fixed += 1
@@ -2603,31 +2703,37 @@ class RemnaWaveService:
subscription.remnawave_short_uuid = rw_user.short_uuid
subscription.subscription_url = rw_user.subscription_url
subscription.subscription_crypto_link = rw_user.happ_crypto_link
- logger.info(f'🔧 Восстановлены данные Remnawave для {user.telegram_id}')
+ logger.info(
+ '🔧 Восстановлены данные Remnawave для', telegram_id=user.telegram_id
+ )
issues_fixed += 1
except Exception as rw_error:
logger.warning(
- f'⚠️ Не удалось получить данные Remnawave для {user.telegram_id}: {rw_error}'
+ '⚠️ Не удалось получить данные Remnawave для',
+ telegram_id=user.telegram_id,
+ rw_error=rw_error,
)
if subscription.traffic_limit_gb < 0:
subscription.traffic_limit_gb = 0
- logger.info(f'🔧 Исправлен некорректный лимит трафика для {user.telegram_id}')
+ logger.info('🔧 Исправлен некорректный лимит трафика для', telegram_id=user.telegram_id)
issues_fixed += 1
if subscription.traffic_used_gb < 0:
subscription.traffic_used_gb = 0.0
- logger.info(f'🔧 Исправлено некорректное использование трафика для {user.telegram_id}')
+ logger.info(
+ '🔧 Исправлено некорректное использование трафика для', telegram_id=user.telegram_id
+ )
issues_fixed += 1
if subscription.device_limit <= 0:
subscription.device_limit = 1
- logger.info(f'🔧 Исправлен лимит устройств для {user.telegram_id}')
+ logger.info('🔧 Исправлен лимит устройств для', telegram_id=user.telegram_id)
issues_fixed += 1
if subscription.connected_squads is None:
subscription.connected_squads = []
- logger.info(f'🔧 Инициализирован список сквадов для {user.telegram_id}')
+ logger.info('🔧 Инициализирован список сквадов для', telegram_id=user.telegram_id)
issues_fixed += 1
if issues_fixed > 0:
@@ -2636,7 +2742,9 @@ class RemnaWaveService:
await db.commit()
except Exception as sub_error:
- logger.error(f'❌ Ошибка валидации подписки {subscription.id}: {sub_error}')
+ logger.error(
+ '❌ Ошибка валидации подписки', subscription_id=subscription.id, sub_error=sub_error
+ )
stats['errors'] += 1
await db.rollback()
@@ -2645,12 +2753,16 @@ class RemnaWaveService:
break
logger.info(
- f'🔍 Валидация завершена: проверено {stats["checked"]}, исправлено подписок {stats["fixed"]}, найдено проблем {stats["issues_found"]}, ошибок {stats["errors"]}'
+ '🔍 Валидация завершена: проверено исправлено подписок найдено проблем ошибок',
+ stats=stats['checked'],
+ stats_2=stats['fixed'],
+ stats_3=stats['issues_found'],
+ stats_4=stats['errors'],
)
return stats
except Exception as e:
- logger.error(f'❌ Критическая ошибка валидации: {e}')
+ logger.error('❌ Критическая ошибка валидации', error=e)
return {'fixed': 0, 'errors': 1, 'checked': 0, 'issues_found': 0}
async def get_sync_recommendations(self, db: AsyncSession) -> dict[str, Any]:
@@ -2698,7 +2810,7 @@ class RemnaWaveService:
return recommendations
except Exception as e:
- logger.error(f'❌ Ошибка получения рекомендаций: {e}')
+ logger.error('❌ Ошибка получения рекомендаций', error=e)
return {
'should_sync': True,
'sync_type': 'all',
@@ -2724,7 +2836,7 @@ class RemnaWaveService:
return status_result
except Exception as e:
- logger.error(f'Ошибка мониторинга статуса панели Remnawave: {e}')
+ logger.error('Ошибка мониторинга статуса панели Remnawave', error=e)
return {'status': 'error', 'error': str(e)}
async def _send_status_change_notification(
@@ -2759,10 +2871,12 @@ class RemnaWaveService:
await notification_service.send_remnawave_panel_status_notification(new_status, details)
- logger.info(f'Отправлено уведомление об изменении статуса панели: {old_status} -> {new_status}')
+ logger.info(
+ 'Отправлено уведомление об изменении статуса панели', old_status=old_status, new_status=new_status
+ )
except Exception as e:
- logger.error(f'Ошибка отправки уведомления об изменении статуса: {e}')
+ logger.error('Ошибка отправки уведомления об изменении статуса', error=e)
async def send_manual_status_notification(self, bot, status: str, message: str = ''):
try:
@@ -2781,11 +2895,11 @@ class RemnaWaveService:
await notification_service.send_remnawave_panel_status_notification(status, details)
- logger.info(f'Отправлено ручное уведомление о статусе панели: {status}')
+ logger.info('Отправлено ручное уведомление о статусе панели', status=status)
return True
except Exception as e:
- logger.error(f'Ошибка отправки ручного уведомления: {e}')
+ logger.error('Ошибка отправки ручного уведомления', error=e)
return False
async def get_panel_status_summary(self) -> dict[str, Any]:
@@ -2822,7 +2936,7 @@ class RemnaWaveService:
return summary
except Exception as e:
- logger.error(f'Ошибка получения сводки статуса панели: {e}')
+ logger.error('Ошибка получения сводки статуса панели', error=e)
return {
'status': 'error',
'description': '❌ Ошибка проверки статуса',
@@ -2894,17 +3008,17 @@ class RemnaWaveService:
if result['api_available']:
if attempt > 1:
- logger.info('Панель Remnawave ответила с %s попытки', attempt)
+ logger.info('Панель Remnawave ответила с попытки', attempt=attempt)
return result
last_result = result
if attempt < attempts:
logger.warning(
- 'Панель Remnawave недоступна (попытка %s/%s): %s',
- attempt,
- attempts,
- result.get('api_error') or 'неизвестная ошибка',
+ 'Панель Remnawave недоступна (попытка /)',
+ attempt=attempt,
+ attempts=attempts,
+ result=result.get('api_error') or 'неизвестная ошибка',
)
await asyncio.sleep(1)
@@ -2912,15 +3026,12 @@ class RemnaWaveService:
last_error = error
if attempt < attempts:
logger.warning(
- 'Ошибка проверки здоровья панели (попытка %s/%s): %s',
- attempt,
- attempts,
- error,
+ 'Ошибка проверки здоровья панели (попытка /)', attempt=attempt, attempts=attempts, error=error
)
await asyncio.sleep(1)
continue
- logger.error(f'Ошибка проверки здоровья панели: {error}')
+ logger.error('Ошибка проверки здоровья панели', error=error)
if last_result is not None:
return last_result
diff --git a/app/services/remnawave_sync_service.py b/app/services/remnawave_sync_service.py
index f28f521d..1d111d84 100644
--- a/app/services/remnawave_sync_service.py
+++ b/app/services/remnawave_sync_service.py
@@ -1,10 +1,10 @@
import asyncio
-import logging
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, time, timedelta
from typing import Any
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -17,7 +17,7 @@ from app.services.remnawave_service import (
from app.utils.cache import cache
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@dataclass(frozen=True)
@@ -143,7 +143,7 @@ class RemnaWaveAutoSyncService:
self._last_user_stats = None
self._last_server_stats = None
self._last_run_finished_at = datetime.utcnow()
- logger.error('❌ Автосинхронизация RemnaWave: %s', message)
+ logger.error('❌ Автосинхронизация RemnaWave', message=message)
return {
'started': True,
'success': False,
@@ -158,7 +158,7 @@ class RemnaWaveAutoSyncService:
self._last_user_stats = None
self._last_server_stats = None
self._last_run_finished_at = datetime.utcnow()
- logger.exception('❌ Ошибка автосинхронизации RemnaWave: %s', error)
+ logger.exception('❌ Ошибка автосинхронизации RemnaWave', error=error)
return {
'started': True,
'success': False,
@@ -247,7 +247,7 @@ class RemnaWaveAutoSyncService:
try:
await cache.delete_pattern('available_countries*')
except Exception as error:
- logger.warning('⚠️ Не удалось очистить кеш стран после автосинхронизации: %s', error)
+ logger.warning('⚠️ Не удалось очистить кеш стран после автосинхронизации', error=error)
return {
'created': created,
diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py
index ed147146..400defde 100644
--- a/app/services/remnawave_webhook_service.py
+++ b/app/services/remnawave_webhook_service.py
@@ -9,11 +9,11 @@ Admin events (node, service, crm) send alerts to the admin notification chat.
from __future__ import annotations
import html
-import logging
import re
from datetime import UTC, datetime
from typing import Any
+import structlog
from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import delete
@@ -38,7 +38,7 @@ from app.services.notification_delivery_service import NotificationType, notific
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Mapping from locale text_key to NotificationType for unified delivery
@@ -170,11 +170,11 @@ class RemnaWaveWebhookService:
user_handler = self._user_handlers.get(event_name)
if user_handler:
if db is None:
- logger.error('RemnaWave webhook: DB session required for user event %s', event_name)
+ logger.error('RemnaWave webhook: DB session required for user event', event_name=event_name)
return False
return await self._process_user_event(db, event_name, data, user_handler)
- logger.debug('Unhandled RemnaWave webhook event: %s', event_name)
+ logger.debug('Unhandled RemnaWave webhook event', event_name=event_name)
return False
async def _process_user_event(self, db: AsyncSession, event_name: str, data: dict, handler: Any) -> bool:
@@ -182,10 +182,10 @@ class RemnaWaveWebhookService:
user, subscription = await self._resolve_user_and_subscription(db, data)
if not user:
logger.warning(
- 'RemnaWave webhook: user not found for event %s, data telegramId=%s uuid=%s',
- event_name,
- data.get('telegramId'),
- data.get('uuid'),
+ 'RemnaWave webhook: user not found for event , data telegramId= uuid',
+ event_name=event_name,
+ data=data.get('telegramId'),
+ data_2=data.get('uuid'),
)
return False
@@ -195,9 +195,9 @@ class RemnaWaveWebhookService:
return True
except (StaleDataError, PendingRollbackError):
logger.warning(
- 'RemnaWave webhook %s: entity already deleted for user %s (concurrent deletion)',
- event_name,
- user_id,
+ 'RemnaWave webhook : entity already deleted for user (concurrent deletion)',
+ event_name=event_name,
+ user_id=user_id,
)
try:
await db.rollback()
@@ -205,7 +205,9 @@ class RemnaWaveWebhookService:
pass
return True
except Exception:
- logger.exception('Error processing RemnaWave webhook event %s for user %s', event_name, user_id)
+ logger.exception(
+ 'Error processing RemnaWave webhook event for user', event_name=event_name, user_id=user_id
+ )
try:
await db.rollback()
except Exception:
@@ -215,7 +217,7 @@ class RemnaWaveWebhookService:
async def _process_admin_event(self, event_name: str, data: dict) -> bool:
"""Format and send admin notification for infrastructure events."""
if not self._admin_service.is_enabled:
- logger.debug('Admin notifications disabled, skipping event %s', event_name)
+ logger.debug('Admin notifications disabled, skipping event', event_name=event_name)
return True
title = self._admin_handlers.get(event_name, event_name)
@@ -272,7 +274,7 @@ class RemnaWaveWebhookService:
await self._admin_service.send_webhook_notification('\n'.join(lines))
return True
except Exception:
- logger.exception('Failed to send admin notification for event %s', event_name)
+ logger.exception('Failed to send admin notification for event', event_name=event_name)
return False
# ------------------------------------------------------------------
@@ -398,25 +400,25 @@ class RemnaWaveWebhookService:
per-event toggles from Settings.
"""
if not settings.WEBHOOK_NOTIFY_USER_ENABLED:
- logger.debug('Webhook user notifications disabled globally, skipping %s', text_key)
+ logger.debug('Webhook user notifications disabled globally, skipping', text_key=text_key)
return
setting_key = _TEXT_KEY_TO_SETTING.get(text_key)
if setting_key and not getattr(settings, setting_key, True):
- logger.debug('Webhook notification %s disabled via %s', text_key, setting_key)
+ logger.debug('Webhook notification disabled via', text_key=text_key, setting_key=setting_key)
return
texts = get_texts(user.language)
message = texts.get(text_key)
if not message:
- logger.warning('Missing locale key %s for language %s', text_key, user.language)
+ logger.warning('Missing locale key for language', text_key=text_key, language=user.language)
return
if format_kwargs:
try:
message = message.format(**format_kwargs)
except (KeyError, IndexError):
- logger.warning('Failed to format message %s with kwargs %s', text_key, format_kwargs)
+ logger.warning('Failed to format message with kwargs', text_key=text_key, format_kwargs=format_kwargs)
return
# Append "Close" button to every webhook notification keyboard
@@ -431,7 +433,7 @@ class RemnaWaveWebhookService:
notification_type = _TEXT_KEY_TO_NOTIFICATION_TYPE.get(text_key)
if not notification_type:
- logger.warning('No NotificationType mapping for text_key %s', text_key)
+ logger.warning('No NotificationType mapping for text_key', text_key=text_key)
return
context = {'text_key': text_key, **(format_kwargs or {})}
@@ -446,7 +448,7 @@ class RemnaWaveWebhookService:
telegram_markup=reply_markup,
)
except Exception:
- logger.exception('Notification delivery failed for user %s, text_key %s', user.id, text_key)
+ logger.exception('Notification delivery failed for user , text_key', user_id=user.id, text_key=text_key)
# ------------------------------------------------------------------
# Webhook timestamp helper
@@ -468,7 +470,7 @@ class RemnaWaveWebhookService:
self._stamp_webhook_update(subscription)
if subscription.status != SubscriptionStatus.EXPIRED.value:
await expire_subscription(db, subscription)
- logger.info('Webhook: subscription %s expired for user %s', subscription.id, user.id)
+ logger.info('Webhook: subscription expired for user', subscription_id=subscription.id, user_id=user.id)
else:
await db.commit()
@@ -481,7 +483,7 @@ class RemnaWaveWebhookService:
self._stamp_webhook_update(subscription)
if subscription.status != SubscriptionStatus.DISABLED.value:
await deactivate_subscription(db, subscription)
- logger.info('Webhook: subscription %s disabled for user %s', subscription.id, user.id)
+ logger.info('Webhook: subscription disabled for user', subscription_id=subscription.id, user_id=user.id)
else:
await db.commit()
@@ -494,7 +496,9 @@ class RemnaWaveWebhookService:
self._stamp_webhook_update(subscription)
if subscription.status == SubscriptionStatus.DISABLED.value:
await reactivate_subscription(db, subscription)
- logger.info('Webhook: subscription %s re-enabled for user %s', subscription.id, user.id)
+ logger.info(
+ 'Webhook: subscription re-enabled for user', subscription_id=subscription.id, user_id=user.id
+ )
else:
await db.commit()
@@ -507,7 +511,9 @@ class RemnaWaveWebhookService:
self._stamp_webhook_update(subscription)
if subscription.status == SubscriptionStatus.ACTIVE.value:
await deactivate_subscription(db, subscription)
- logger.info('Webhook: subscription %s limited (traffic) for user %s', subscription.id, user.id)
+ logger.info(
+ 'Webhook: subscription limited (traffic) for user', subscription_id=subscription.id, user_id=user.id
+ )
else:
await db.commit()
@@ -522,7 +528,9 @@ class RemnaWaveWebhookService:
# Re-enable if was disabled due to traffic limit
if subscription.status == SubscriptionStatus.DISABLED.value:
await reactivate_subscription(db, subscription)
- logger.info('Webhook: traffic reset for subscription %s, user %s', subscription.id, user.id)
+ logger.info(
+ 'Webhook: traffic reset for subscription , user', subscription_id=subscription.id, user_id=user.id
+ )
await self._notify_user(user, 'WEBHOOK_SUB_TRAFFIC_RESET', reply_markup=self._get_subscription_keyboard(user))
@@ -578,10 +586,10 @@ class RemnaWaveWebhookService:
subscription.status = SubscriptionStatus.ACTIVE.value
changed = True
logger.info(
- 'Webhook: subscription %s reactivated (%s → active) for user %s',
- subscription.id,
- subscription.status,
- user.id,
+ 'Webhook: subscription reactivated (→ active) for user',
+ subscription_id=subscription.id,
+ subscription_status=subscription.status,
+ user_id=user.id,
)
elif panel_status == 'DISABLED':
if subscription.status != SubscriptionStatus.DISABLED.value:
@@ -602,7 +610,11 @@ class RemnaWaveWebhookService:
self._stamp_webhook_update(subscription)
if changed:
subscription.updated_at = datetime.now(UTC).replace(tzinfo=None)
- logger.info('Webhook: subscription %s modified (synced from panel) for user %s', subscription.id, user.id)
+ logger.info(
+ 'Webhook: subscription modified (synced from panel) for user',
+ subscription_id=subscription.id,
+ user_id=user.id,
+ )
await db.commit()
async def _handle_user_deleted(
@@ -623,9 +635,9 @@ class RemnaWaveWebhookService:
except Exception:
# Subscription was cascade-deleted, re-fetch user and skip subscription updates
logger.warning(
- 'Webhook: subscription %s already deleted for user %s, skipping subscription cleanup',
- sub_id,
- user_id,
+ 'Webhook: subscription already deleted for user , skipping subscription cleanup',
+ sub_id=sub_id,
+ user_id=user_id,
)
subscription = None
try:
@@ -636,19 +648,19 @@ class RemnaWaveWebhookService:
try:
user = await get_user_by_id(db, user_id)
except Exception:
- logger.error('Webhook: user %s not found after rollback', user_id)
+ logger.error('Webhook: user not found after rollback', user_id=user_id)
return
if not user:
- logger.error('Webhook: user %s not found after rollback', user_id)
+ logger.error('Webhook: user not found after rollback', user_id=user_id)
return
if subscription:
if subscription.status != SubscriptionStatus.EXPIRED.value:
subscription.status = SubscriptionStatus.EXPIRED.value
logger.info(
- 'Webhook: subscription %s marked expired (user deleted in panel) for user %s',
- sub_id,
- user_id,
+ 'Webhook: subscription marked expired (user deleted in panel) for user',
+ sub_id=sub_id,
+ user_id=user_id,
)
# Clear subscription data — panel user no longer exists
@@ -693,7 +705,9 @@ class RemnaWaveWebhookService:
if changed:
subscription.updated_at = datetime.now(UTC).replace(tzinfo=None)
logger.info(
- 'Webhook: subscription %s credentials revoked/updated for user %s', subscription.id, user.id
+ 'Webhook: subscription credentials revoked/updated for user',
+ subscription_id=subscription.id,
+ user_id=user.id,
)
await db.commit()
@@ -702,7 +716,7 @@ class RemnaWaveWebhookService:
async def _handle_user_created(
self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict
) -> None:
- logger.info('Webhook: user %s created externally in panel (uuid=%s)', user.id, data.get('uuid'))
+ logger.info('Webhook: user created externally in panel (uuid=)', user_id=user.id, data=data.get('uuid'))
async def _handle_expires_in_72h(
self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict
@@ -727,7 +741,7 @@ class RemnaWaveWebhookService:
async def _handle_first_connected(
self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict
) -> None:
- logger.info('Webhook: user %s first VPN connection', user.id)
+ logger.info('Webhook: user first VPN connection', user_id=user.id)
await self._notify_user(user, 'WEBHOOK_SUB_FIRST_CONNECTED', reply_markup=self._get_subscription_keyboard(user))
async def _handle_bandwidth_threshold(
@@ -754,7 +768,7 @@ class RemnaWaveWebhookService:
async def _handle_user_not_connected(
self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict
) -> None:
- logger.info('Webhook: user %s has not connected to VPN', user.id)
+ logger.info('Webhook: user has not connected to VPN', user_id=user.id)
await self._notify_user(user, 'WEBHOOK_USER_NOT_CONNECTED', reply_markup=self._get_connect_keyboard(user))
# ------------------------------------------------------------------
@@ -797,7 +811,7 @@ class RemnaWaveWebhookService:
self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict
) -> None:
device_name = self._extract_device_name(data)
- logger.info('Webhook: device added for user %s: %s', user.id, device_name or '(empty)')
+ logger.info('Webhook: device added for user', user_id=user.id, device_name=device_name or '(empty)')
await self._notify_user(
user,
'WEBHOOK_DEVICE_ADDED',
@@ -809,7 +823,7 @@ class RemnaWaveWebhookService:
self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict
) -> None:
device_name = self._extract_device_name(data)
- logger.info('Webhook: device deleted for user %s: %s', user.id, device_name or '(empty)')
+ logger.info('Webhook: device deleted for user', user_id=user.id, device_name=device_name or '(empty)')
await self._notify_user(
user,
'WEBHOOK_DEVICE_DELETED',
diff --git a/app/services/reporting_service.py b/app/services/reporting_service.py
index 7e22adbc..9d77379a 100644
--- a/app/services/reporting_service.py
+++ b/app/services/reporting_service.py
@@ -1,11 +1,11 @@
import asyncio
-import logging
from dataclasses import dataclass
from datetime import UTC, date, datetime, time as datetime_time, timedelta
from enum import Enum
from html import escape
from zoneinfo import ZoneInfo
+import structlog
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from sqlalchemy import cast, func, not_, or_, select
@@ -28,7 +28,7 @@ from app.database.models import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class ReportingServiceError(RuntimeError):
@@ -84,10 +84,7 @@ class ReportingService:
return
self._task = asyncio.create_task(self._auto_daily_loop(send_time))
- logger.info(
- '📊 Сервис отчетов запущен: ежедневная отправка в %s по МСК',
- send_time.strftime('%H:%M'),
- )
+ logger.info('📊 Сервис отчетов запущен: ежедневная отправка в по МСК', send_time=send_time.strftime('%H:%M'))
async def stop(self) -> None:
if self._task and not self._task.done():
@@ -129,14 +126,11 @@ class ReportingService:
report_date=report_date,
send_to_topic=True,
)
- logger.info(
- '📊 Автоматический отчет за %s отправлен',
- report_date.strftime('%d.%m.%Y'),
- )
+ logger.info('📊 Автоматический отчет за отправлен', report_date=report_date.strftime('%d.%m.%Y'))
except asyncio.CancelledError:
raise
except Exception as exc:
- logger.error('Ошибка автоматической отправки отчета: %s', exc)
+ logger.error('Ошибка автоматической отправки отчета', exc=exc)
next_run_utc, report_date = self._calculate_next_run(send_time)
@@ -144,7 +138,7 @@ class ReportingService:
logger.info('Сервис отчетов остановлен')
raise
except Exception as exc:
- logger.error('Критическая ошибка в сервисе отчетов: %s', exc)
+ logger.error('Критическая ошибка в сервисе отчетов', exc=exc)
def _calculate_next_run(
self,
@@ -177,7 +171,7 @@ class ReportingService:
parse_mode='HTML',
)
except (TelegramBadRequest, TelegramForbiddenError) as exc:
- logger.error('Не удалось отправить отчет: %s', exc)
+ logger.error('Не удалось отправить отчет', exc=exc)
raise ReportingServiceError('Не удалось отправить отчет в чат') from exc
# ---------- referral helpers ----------
diff --git a/app/services/server_status_service.py b/app/services/server_status_service.py
index e88e57cd..54ab826a 100644
--- a/app/services/server_status_service.py
+++ b/app/services/server_status_service.py
@@ -1,15 +1,15 @@
from __future__ import annotations
-import logging
import re
from dataclasses import dataclass
import aiohttp
+import structlog
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@dataclass
@@ -35,7 +35,7 @@ class ServerStatusService:
_FLAG_PATTERN = re.compile(r'^([\U0001F1E6-\U0001F1FF]{2})\s*(.*)$')
def __init__(self) -> None:
- self._logger = logging.getLogger(self.__class__.__name__)
+ pass
async def get_servers(self) -> list[ServerStatusEntry]:
mode = settings.get_server_status_mode()
diff --git a/app/services/startup_notification_service.py b/app/services/startup_notification_service.py
index fc7d26aa..1d8b77d4 100644
--- a/app/services/startup_notification_service.py
+++ b/app/services/startup_notification_service.py
@@ -4,10 +4,10 @@
Отправляет красивое сообщение с информацией о системе при запуске бота.
"""
-import logging
from datetime import datetime
from typing import Final
+import structlog
from aiogram import Bot
from aiogram.enums import ParseMode
from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup
@@ -20,7 +20,7 @@ from app.external.remnawave_api import RemnaWaveAPI, test_api_connection
from app.utils.timezone import format_local_datetime
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Константы
DEFAULT_VERSION: Final[str] = 'dev'
@@ -91,7 +91,7 @@ class StartupNotificationService:
result = await db.execute(select(func.count(User.id)).where(User.status == UserStatus.ACTIVE.value))
return result.scalar() or 0
except Exception as e:
- logger.error('Ошибка получения количества пользователей: %s', e)
+ logger.error('Ошибка получения количества пользователей', e=e)
return 0
async def _get_total_balance(self) -> int:
@@ -105,7 +105,7 @@ class StartupNotificationService:
)
return result.scalar() or 0
except Exception as e:
- logger.error('Ошибка получения суммы балансов: %s', e)
+ logger.error('Ошибка получения суммы балансов', e=e)
return 0
async def _get_open_tickets_count(self) -> int:
@@ -115,7 +115,7 @@ class StartupNotificationService:
result = await db.execute(select(func.count(Ticket.id)).where(Ticket.status == TicketStatus.OPEN.value))
return result.scalar() or 0
except Exception as e:
- logger.error('Ошибка получения количества открытых тикетов: %s', e)
+ logger.error('Ошибка получения количества открытых тикетов', e=e)
return 0
async def _get_paid_subscriptions_count(self) -> int:
@@ -130,7 +130,7 @@ class StartupNotificationService:
)
return result.scalar() or 0
except Exception as e:
- logger.error('Ошибка получения количества платных подписок: %s', e)
+ logger.error('Ошибка получения количества платных подписок', e=e)
return 0
async def _get_trial_subscriptions_count(self) -> int:
@@ -140,7 +140,7 @@ class StartupNotificationService:
result = await db.execute(select(func.count(Subscription.id)).where(Subscription.is_trial == True))
return result.scalar() or 0
except Exception as e:
- logger.error('Ошибка получения количества триальных подписок: %s', e)
+ logger.error('Ошибка получения количества триальных подписок', e=e)
return 0
async def _check_remnawave_connection(self) -> tuple[bool, str]:
@@ -181,7 +181,7 @@ class StartupNotificationService:
return False, 'Недоступна'
except Exception as e:
- logger.error('Ошибка проверки соединения с Remnawave: %s', e)
+ logger.error('Ошибка проверки соединения с Remnawave', e=e)
return False, 'Ошибка подключения'
def _format_balance(self, kopeks: int) -> str:
@@ -272,11 +272,11 @@ class StartupNotificationService:
message_kwargs['message_thread_id'] = self.topic_id
await self.bot.send_message(**message_kwargs)
- logger.info('Стартовое уведомление отправлено в чат %s', self.chat_id)
+ logger.info('Стартовое уведомление отправлено в чат', chat_id=self.chat_id)
return True
except Exception as e:
- logger.error('Ошибка отправки стартового уведомления: %s', e)
+ logger.error('Ошибка отправки стартового уведомления', e=e)
return False
@@ -452,9 +452,9 @@ async def send_crash_notification(bot: Bot, error: Exception, traceback_str: str
message_kwargs['message_thread_id'] = topic_id
await bot.send_document(**message_kwargs)
- logger.info('Уведомление о падении отправлено в чат %s', chat_id)
+ logger.info('Уведомление о падении отправлено в чат', chat_id=chat_id)
return True
except Exception as e:
- logger.error('Ошибка отправки уведомления о падении: %s', e)
+ logger.error('Ошибка отправки уведомления о падении', e=e)
return False
diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py
index 9808cf0c..ef10ed44 100644
--- a/app/services/subscription_auto_purchase_service.py
+++ b/app/services/subscription_auto_purchase_service.py
@@ -2,9 +2,9 @@
from __future__ import annotations
-import logging
from dataclasses import dataclass
+import structlog
from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy.ext.asyncio import AsyncSession
@@ -31,7 +31,7 @@ from app.utils.pricing_utils import format_period_description
from app.utils.timezone import format_local_datetime
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _format_user_id(user: User) -> str:
@@ -75,8 +75,8 @@ async def _prepare_auto_purchase(
period_days = int(cart_data.get('period_days') or 0)
if period_days <= 0:
logger.info(
- '🔁 Автопокупка: у пользователя %s нет корректного периода в сохранённой корзине',
- _format_user_id(user),
+ '🔁 Автопокупка: у пользователя нет корректного периода в сохранённой корзине',
+ format_user_id=_format_user_id(user),
)
return None
@@ -84,10 +84,7 @@ async def _prepare_auto_purchase(
# т.к. после db.refresh() в payment-сервисах связи сбрасываются
fresh_user = await get_user_by_id(db, user.id)
if not fresh_user:
- logger.warning(
- '🔁 Автопокупка: не удалось перезагрузить пользователя %s',
- _format_user_id(user),
- )
+ logger.warning('🔁 Автопокупка: не удалось перезагрузить пользователя', format_user_id=_format_user_id(user))
return None
user = fresh_user
@@ -97,9 +94,9 @@ async def _prepare_auto_purchase(
period_config = context.period_map.get(f'days:{period_days}')
if not period_config:
logger.warning(
- '🔁 Автопокупка: период %s дней недоступен для пользователя %s',
- period_days,
- _format_user_id(user),
+ '🔁 Автопокупка: период дней недоступен для пользователя',
+ period_days=period_days,
+ format_user_id=_format_user_id(user),
)
return None
@@ -162,9 +159,9 @@ async def _get_tariff_price_for_period(
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff or not tariff.is_active:
logger.warning(
- '🔁 Автопокупка: тариф %s недоступен для пользователя %s',
- tariff_id,
- _format_user_id(user),
+ '🔁 Автопокупка: тариф недоступен для пользователя',
+ tariff_id=tariff_id,
+ format_user_id=_format_user_id(user),
)
return None
@@ -172,9 +169,7 @@ async def _get_tariff_price_for_period(
base_price = prices.get(str(period_days))
if base_price is None:
logger.warning(
- '🔁 Автопокупка: период %s дней недоступен для тарифа %s',
- period_days,
- tariff_id,
+ '🔁 Автопокупка: период дней недоступен для тарифа', period_days=period_days, tariff_id=tariff_id
)
return None
@@ -201,8 +196,7 @@ async def _prepare_auto_extend_context(
subscription = await get_subscription_by_user_id(db, user.id)
if subscription is None:
logger.info(
- '🔁 Автопокупка: у пользователя %s нет активной подписки для продления',
- _format_user_id(user),
+ '🔁 Автопокупка: у пользователя нет активной подписки для продления', format_user_id=_format_user_id(user)
)
return None
@@ -211,10 +205,10 @@ async def _prepare_auto_extend_context(
saved_subscription_id = _safe_int(saved_subscription_id, subscription.id)
if saved_subscription_id != subscription.id:
logger.warning(
- '🔁 Автопокупка: сохранённая подписка %s не совпадает с текущей %s у пользователя %s',
- saved_subscription_id,
- subscription.id,
- _format_user_id(user),
+ '🔁 Автопокупка: сохранённая подписка не совпадает с текущей у пользователя',
+ saved_subscription_id=saved_subscription_id,
+ subscription_id=subscription.id,
+ format_user_id=_format_user_id(user),
)
return None
@@ -222,9 +216,9 @@ async def _prepare_auto_extend_context(
if period_days <= 0:
logger.warning(
- '🔁 Автопокупка: некорректное количество дней продления (%s) у пользователя %s',
- period_days,
- _format_user_id(user),
+ '🔁 Автопокупка: некорректное количество дней продления у пользователя',
+ period_days=period_days,
+ format_user_id=_format_user_id(user),
)
return None
@@ -239,9 +233,9 @@ async def _prepare_auto_extend_context(
cart_data.get('total_price') or cart_data.get('price') or cart_data.get('final_price'),
)
logger.warning(
- '🔁 Автопокупка: не удалось пересчитать цену тарифа %s, используем сохранённую: %s',
- tariff_id,
- price_kopeks,
+ '🔁 Автопокупка: не удалось пересчитать цену тарифа , используем сохранённую',
+ tariff_id=tariff_id,
+ price_kopeks=price_kopeks,
)
# Добавляем стоимость докупленных устройств при продлении того же тарифа
elif subscription.tariff_id == tariff_id:
@@ -263,9 +257,9 @@ async def _prepare_auto_extend_context(
if price_kopeks <= 0:
logger.warning(
- '🔁 Автопокупка: некорректная цена продления (%s) у пользователя %s',
- price_kopeks,
- _format_user_id(user),
+ '🔁 Автопокупка: некорректная цена продления у пользователя',
+ price_kopeks=price_kopeks,
+ format_user_id=_format_user_id(user),
)
return None
@@ -354,9 +348,9 @@ async def _auto_extend_subscription(
prepared = await _prepare_auto_extend_context(db, user, cart_data)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '❌ Автопокупка: ошибка подготовки данных продления для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка: ошибка подготовки данных продления для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
@@ -366,10 +360,10 @@ async def _auto_extend_subscription(
if user.balance_kopeks < prepared.price_kopeks:
logger.info(
- '🔁 Автопокупка: у пользователя %s недостаточно средств для продления (%s < %s)',
- _format_user_id(user),
- user.balance_kopeks,
- prepared.price_kopeks,
+ '🔁 Автопокупка: у пользователя недостаточно средств для продления (<)',
+ format_user_id=_format_user_id(user),
+ balance_kopeks=user.balance_kopeks,
+ price_kopeks=prepared.price_kopeks,
)
return False
@@ -383,17 +377,17 @@ async def _auto_extend_subscription(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '❌ Автопокупка: ошибка списания средств при продлении пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка: ошибка списания средств при продлении пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
if not deducted:
logger.warning(
- '❌ Автопокупка: списание средств для продления подписки пользователя %s не выполнено',
- _format_user_id(user),
+ '❌ Автопокупка: списание средств для продления подписки пользователя не выполнено',
+ format_user_id=_format_user_id(user),
)
return False
@@ -425,16 +419,16 @@ async def _auto_extend_subscription(
user.has_had_paid_subscription = True
await db.commit()
logger.info(
- '✅ Триал конвертирован в платную подписку %s для пользователя %s',
- subscription.id,
- _format_user_id(user),
+ '✅ Триал конвертирован в платную подписку для пользователя',
+ subscription_id=subscription.id,
+ format_user_id=_format_user_id(user),
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '❌ Автопокупка: не удалось продлить подписку пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка: не удалось продлить подписку пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
# НОВОЕ: Откатываем изменения при ошибке
@@ -452,9 +446,9 @@ async def _auto_extend_subscription(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '⚠️ Автопокупка: не удалось зафиксировать транзакцию продления для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка: не удалось зафиксировать транзакцию продления для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
@@ -470,9 +464,9 @@ async def _auto_extend_subscription(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '⚠️ Автопокупка: не удалось обновить RemnaWave пользователя %s после продления: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка: не удалось обновить RemnaWave пользователя после продления',
+ format_user_id=_format_user_id(user),
+ error=error,
)
await user_cart_service.delete_user_cart(user.id)
@@ -501,9 +495,9 @@ async def _auto_extend_subscription(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '⚠️ Автопокупка: не удалось уведомить администраторов о продлении пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка: не удалось уведомить администраторов о продлении пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Send user notification only for Telegram users
@@ -551,15 +545,15 @@ async def _auto_extend_subscription(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '⚠️ Автопокупка: не удалось уведомить пользователя %s о продлении: %s',
- user.telegram_id or user.id,
- error,
+ '⚠️ Автопокупка: не удалось уведомить пользователя о продлении',
+ telegram_id=user.telegram_id or user.id,
+ error=error,
)
logger.info(
- '✅ Автопокупка: подписка продлена на %s дней для пользователя %s',
- prepared.period_days,
- _format_user_id(user),
+ '✅ Автопокупка: подписка продлена на дней для пользователя',
+ period_days=prepared.period_days,
+ format_user_id=_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
@@ -571,9 +565,9 @@ async def _auto_extend_subscription(
)
except Exception as ws_error:
logger.warning(
- '⚠️ Автопокупка: не удалось отправить WS уведомление о продлении для %s: %s',
- _format_user_id(user),
- ws_error,
+ '⚠️ Автопокупка: не удалось отправить WS уведомление о продлении для',
+ format_user_id=_format_user_id(user),
+ ws_error=ws_error,
)
return True
@@ -609,19 +603,19 @@ async def _auto_purchase_tariff(
if not tariff_id or period_days <= 0:
logger.warning(
- '🔁 Автопокупка тарифа: некорректные данные корзины для пользователя %s (tariff_id=%s, period=%s)',
- _format_user_id(user),
- tariff_id,
- period_days,
+ '🔁 Автопокупка тарифа: некорректные данные корзины для пользователя (tariff_id period=)',
+ format_user_id=_format_user_id(user),
+ tariff_id=tariff_id,
+ period_days=period_days,
)
return False
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff or not tariff.is_active:
logger.warning(
- '🔁 Автопокупка тарифа: тариф %s недоступен для пользователя %s',
- tariff_id,
- _format_user_id(user),
+ '🔁 Автопокупка тарифа: тариф недоступен для пользователя',
+ tariff_id=tariff_id,
+ format_user_id=_format_user_id(user),
)
return False
@@ -630,9 +624,7 @@ async def _auto_purchase_tariff(
base_price = prices.get(str(period_days))
if base_price is None:
logger.warning(
- '🔁 Автопокупка тарифа: период %s дней недоступен для тарифа %s',
- period_days,
- tariff_id,
+ '🔁 Автопокупка тарифа: период дней недоступен для тарифа', period_days=period_days, tariff_id=tariff_id
)
return False
@@ -654,10 +646,10 @@ async def _auto_purchase_tariff(
if user.balance_kopeks < final_price:
logger.info(
- '🔁 Автопокупка тарифа: у пользователя %s недостаточно средств (%s < %s)',
- _format_user_id(user),
- user.balance_kopeks,
- final_price,
+ '🔁 Автопокупка тарифа: у пользователя недостаточно средств (<)',
+ format_user_id=_format_user_id(user),
+ balance_kopeks=user.balance_kopeks,
+ final_price=final_price,
)
return False
@@ -667,15 +659,14 @@ async def _auto_purchase_tariff(
success = await subtract_user_balance(db, user, final_price, description)
if not success:
logger.warning(
- '❌ Автопокупка тарифа: не удалось списать баланс пользователя %s',
- _format_user_id(user),
+ '❌ Автопокупка тарифа: не удалось списать баланс пользователя', format_user_id=_format_user_id(user)
)
return False
except Exception as error:
logger.error(
- '❌ Автопокупка тарифа: ошибка списания баланса пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка тарифа: ошибка списания баланса пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
@@ -723,9 +714,9 @@ async def _auto_purchase_tariff(
was_trial_conversion = False
except Exception as error:
logger.error(
- '❌ Автопокупка тарифа: ошибка создания подписки для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка тарифа: ошибка создания подписки для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
await db.rollback()
@@ -742,9 +733,9 @@ async def _auto_purchase_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка тарифа: не удалось создать транзакцию для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка тарифа: не удалось создать транзакцию для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
transaction = None
@@ -760,9 +751,9 @@ async def _auto_purchase_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка тарифа: не удалось обновить Remnawave для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка тарифа: не удалось обновить Remnawave для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Очищаем корзину
@@ -781,9 +772,9 @@ async def _auto_purchase_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка тарифа: не удалось уведомить админов о покупке пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка тарифа: не удалось уведомить админов о покупке пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Send user notification only for Telegram users
@@ -824,16 +815,16 @@ async def _auto_purchase_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка тарифа: не удалось уведомить пользователя %s: %s',
- user.telegram_id or user.id,
- error,
+ '⚠️ Автопокупка тарифа: не удалось уведомить пользователя',
+ telegram_id=user.telegram_id or user.id,
+ error=error,
)
logger.info(
- '✅ Автопокупка тарифа: подписка на тариф %s (%s дней) оформлена для пользователя %s',
- tariff.name,
- period_days,
- _format_user_id(user),
+ '✅ Автопокупка тарифа: подписка на тариф (дней) оформлена для пользователя',
+ tariff_name=tariff.name,
+ period_days=period_days,
+ format_user_id=_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
@@ -854,9 +845,9 @@ async def _auto_purchase_tariff(
)
except Exception as ws_error:
logger.warning(
- '⚠️ Автопокупка тарифа: не удалось отправить WS уведомление для %s: %s',
- _format_user_id(user),
- ws_error,
+ '⚠️ Автопокупка тарифа: не удалось отправить WS уведомление для',
+ format_user_id=_format_user_id(user),
+ ws_error=ws_error,
)
return True
@@ -887,43 +878,43 @@ async def _auto_purchase_daily_tariff(
tariff_id = _safe_int(cart_data.get('tariff_id'))
if not tariff_id:
logger.warning(
- '🔁 Автопокупка суточного тарифа: нет tariff_id в корзине пользователя %s',
- _format_user_id(user),
+ '🔁 Автопокупка суточного тарифа: нет tariff_id в корзине пользователя',
+ format_user_id=_format_user_id(user),
)
return False
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff or not tariff.is_active:
logger.warning(
- '🔁 Автопокупка суточного тарифа: тариф %s недоступен для пользователя %s',
- tariff_id,
- _format_user_id(user),
+ '🔁 Автопокупка суточного тарифа: тариф недоступен для пользователя',
+ tariff_id=tariff_id,
+ format_user_id=_format_user_id(user),
)
return False
if not getattr(tariff, 'is_daily', False):
logger.warning(
- '🔁 Автопокупка суточного тарифа: тариф %s не является суточным для пользователя %s',
- tariff_id,
- _format_user_id(user),
+ '🔁 Автопокупка суточного тарифа: тариф не является суточным для пользователя',
+ tariff_id=tariff_id,
+ format_user_id=_format_user_id(user),
)
return False
daily_price = getattr(tariff, 'daily_price_kopeks', 0)
if daily_price <= 0:
logger.warning(
- '🔁 Автопокупка суточного тарифа: некорректная цена тарифа %s для пользователя %s',
- tariff_id,
- _format_user_id(user),
+ '🔁 Автопокупка суточного тарифа: некорректная цена тарифа для пользователя',
+ tariff_id=tariff_id,
+ format_user_id=_format_user_id(user),
)
return False
if user.balance_kopeks < daily_price:
logger.info(
- '🔁 Автопокупка суточного тарифа: у пользователя %s недостаточно средств (%s < %s)',
- _format_user_id(user),
- user.balance_kopeks,
- daily_price,
+ '🔁 Автопокупка суточного тарифа: у пользователя недостаточно средств (<)',
+ format_user_id=_format_user_id(user),
+ balance_kopeks=user.balance_kopeks,
+ daily_price=daily_price,
)
return False
@@ -933,15 +924,15 @@ async def _auto_purchase_daily_tariff(
success = await subtract_user_balance(db, user, daily_price, description)
if not success:
logger.warning(
- '❌ Автопокупка суточного тарифа: не удалось списать баланс пользователя %s',
- _format_user_id(user),
+ '❌ Автопокупка суточного тарифа: не удалось списать баланс пользователя',
+ format_user_id=_format_user_id(user),
)
return False
except Exception as error:
logger.error(
- '❌ Автопокупка суточного тарифа: ошибка списания баланса пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка суточного тарифа: ошибка списания баланса пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
@@ -993,9 +984,9 @@ async def _auto_purchase_daily_tariff(
was_trial_conversion = False
except Exception as error:
logger.error(
- '❌ Автопокупка суточного тарифа: ошибка создания подписки для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка суточного тарифа: ошибка создания подписки для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
await db.rollback()
@@ -1012,9 +1003,9 @@ async def _auto_purchase_daily_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка суточного тарифа: не удалось создать транзакцию для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка суточного тарифа: не удалось создать транзакцию для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
transaction = None
@@ -1030,9 +1021,9 @@ async def _auto_purchase_daily_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка суточного тарифа: не удалось обновить Remnawave для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка суточного тарифа: не удалось обновить Remnawave для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Очищаем корзину
@@ -1050,9 +1041,9 @@ async def _auto_purchase_daily_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка суточного тарифа: не удалось уведомить админов о покупке пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка суточного тарифа: не удалось уведомить админов о покупке пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Send user notification only for Telegram users
@@ -1090,15 +1081,15 @@ async def _auto_purchase_daily_tariff(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка суточного тарифа: не удалось уведомить пользователя %s: %s',
- user.telegram_id or user.id,
- error,
+ '⚠️ Автопокупка суточного тарифа: не удалось уведомить пользователя',
+ telegram_id=user.telegram_id or user.id,
+ error=error,
)
logger.info(
- '✅ Автопокупка суточного тарифа: тариф %s активирован для пользователя %s',
- tariff.name,
- _format_user_id(user),
+ '✅ Автопокупка суточного тарифа: тариф активирован для пользователя',
+ tariff_name=tariff.name,
+ format_user_id=_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
@@ -1119,9 +1110,9 @@ async def _auto_purchase_daily_tariff(
)
except Exception as ws_error:
logger.warning(
- '⚠️ Автопокупка суточного тарифа: не удалось отправить WS уведомление для %s: %s',
- _format_user_id(user),
- ws_error,
+ '⚠️ Автопокупка суточного тарифа: не удалось отправить WS уведомление для',
+ format_user_id=_format_user_id(user),
+ ws_error=ws_error,
)
return True
@@ -1146,38 +1137,35 @@ async def _auto_add_devices(
if devices_to_add <= 0 or price_kopeks <= 0:
logger.warning(
- '🔁 Автопокупка устройств: некорректные данные корзины для пользователя %s (devices=%s, price=%s)',
- _format_user_id(user),
- devices_to_add,
- price_kopeks,
+ '🔁 Автопокупка устройств: некорректные данные корзины для пользователя (devices price=)',
+ format_user_id=_format_user_id(user),
+ devices_to_add=devices_to_add,
+ price_kopeks=price_kopeks,
)
return False
# Проверяем баланс
if user.balance_kopeks < price_kopeks:
logger.info(
- '🔁 Автопокупка устройств: у пользователя %s недостаточно средств (%s < %s)',
- _format_user_id(user),
- user.balance_kopeks,
- price_kopeks,
+ '🔁 Автопокупка устройств: у пользователя недостаточно средств (<)',
+ format_user_id=_format_user_id(user),
+ balance_kopeks=user.balance_kopeks,
+ price_kopeks=price_kopeks,
)
return False
# Проверяем подписку
subscription = await get_subscription_by_user_id(db, user.id)
if not subscription:
- logger.warning(
- '🔁 Автопокупка устройств: у пользователя %s нет подписки',
- _format_user_id(user),
- )
+ logger.warning('🔁 Автопокупка устройств: у пользователя нет подписки', format_user_id=_format_user_id(user))
await user_cart_service.delete_user_cart(user.id)
return False
if subscription.status not in ('active', 'trial', 'ACTIVE', 'TRIAL'):
logger.warning(
- '🔁 Автопокупка устройств: подписка пользователя %s не активна (status=%s)',
- _format_user_id(user),
- subscription.status,
+ '🔁 Автопокупка устройств: подписка пользователя не активна (status=)',
+ format_user_id=_format_user_id(user),
+ subscription_status=subscription.status,
)
await user_cart_service.delete_user_cart(user.id)
return False
@@ -1195,15 +1183,14 @@ async def _auto_add_devices(
)
if not success:
logger.warning(
- '❌ Автопокупка устройств: не удалось списать баланс пользователя %s',
- _format_user_id(user),
+ '❌ Автопокупка устройств: не удалось списать баланс пользователя', format_user_id=_format_user_id(user)
)
return False
except Exception as error:
logger.error(
- '❌ Автопокупка устройств: ошибка списания баланса пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка устройств: ошибка списания баланса пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
@@ -1217,9 +1204,9 @@ async def _auto_add_devices(
await db.refresh(subscription)
except Exception as error:
logger.error(
- '❌ Автопокупка устройств: ошибка сохранения подписки пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка устройств: ошибка сохранения подписки пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
await db.rollback()
@@ -1231,21 +1218,21 @@ async def _auto_add_devices(
await subscription_service.update_remnawave_user(db, subscription)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка устройств: не удалось обновить Remnawave для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка устройств: не удалось обновить Remnawave для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Очищаем корзину (транзакция уже создана в subtract_user_balance)
await user_cart_service.delete_user_cart(user.id)
logger.info(
- '✅ Автопокупка устройств: пользователь %s добавил %s устройств (было %s, стало %s) за %s коп.',
- _format_user_id(user),
- devices_to_add,
- old_device_limit,
- subscription.device_limit,
- price_kopeks,
+ '✅ Автопокупка устройств: пользователь добавил устройств (было , стало) за коп.',
+ format_user_id=_format_user_id(user),
+ devices_to_add=devices_to_add,
+ old_device_limit=old_device_limit,
+ device_limit=subscription.device_limit,
+ price_kopeks=price_kopeks,
)
# WebSocket уведомление для кабинета
@@ -1259,10 +1246,7 @@ async def _auto_add_devices(
amount_kopeks=price_kopeks,
)
except Exception as ws_error:
- logger.warning(
- '⚠️ Автопокупка устройств: не удалось отправить WebSocket уведомление: %s',
- ws_error,
- )
+ logger.warning('⚠️ Автопокупка устройств: не удалось отправить WebSocket уведомление', ws_error=ws_error)
# Уведомление пользователю
if bot and user.telegram_id:
@@ -1307,9 +1291,7 @@ async def _auto_add_devices(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка устройств: не удалось уведомить пользователя %s: %s',
- user.telegram_id,
- error,
+ '⚠️ Автопокупка устройств: не удалось уведомить пользователя', telegram_id=user.telegram_id, error=error
)
# Уведомление админам
@@ -1326,10 +1308,7 @@ async def _auto_add_devices(
price_kopeks,
)
except Exception as error:
- logger.warning(
- '⚠️ Автопокупка устройств: не удалось уведомить админов: %s',
- error,
- )
+ logger.warning('⚠️ Автопокупка устройств: не удалось уведомить админов', error=error)
return True
@@ -1353,54 +1332,47 @@ async def _auto_add_traffic(
if traffic_gb <= 0 or price_kopeks <= 0:
logger.warning(
- '🔁 Автопокупка трафика: некорректные данные корзины для пользователя %s (traffic_gb=%s, price=%s)',
- _format_user_id(user),
- traffic_gb,
- price_kopeks,
+ '🔁 Автопокупка трафика: некорректные данные корзины для пользователя (traffic_gb price=)',
+ format_user_id=_format_user_id(user),
+ traffic_gb=traffic_gb,
+ price_kopeks=price_kopeks,
)
return False
# Verify balance
if user.balance_kopeks < price_kopeks:
logger.info(
- '🔁 Автопокупка трафика: у пользователя %s недостаточно средств (%s < %s)',
- _format_user_id(user),
- user.balance_kopeks,
- price_kopeks,
+ '🔁 Автопокупка трафика: у пользователя недостаточно средств (<)',
+ format_user_id=_format_user_id(user),
+ balance_kopeks=user.balance_kopeks,
+ price_kopeks=price_kopeks,
)
return False
# Verify subscription
subscription = await get_subscription_by_user_id(db, user.id)
if not subscription:
- logger.warning(
- '🔁 Автопокупка трафика: у пользователя %s нет подписки',
- _format_user_id(user),
- )
+ logger.warning('🔁 Автопокупка трафика: у пользователя нет подписки', format_user_id=_format_user_id(user))
await user_cart_service.delete_user_cart(user.id)
return False
if subscription.status not in ('active', 'trial', 'ACTIVE', 'TRIAL'):
logger.warning(
- '🔁 Автопокупка трафика: подписка пользователя %s не активна (status=%s)',
- _format_user_id(user),
- subscription.status,
+ '🔁 Автопокупка трафика: подписка пользователя не активна (status=)',
+ format_user_id=_format_user_id(user),
+ subscription_status=subscription.status,
)
await user_cart_service.delete_user_cart(user.id)
return False
if subscription.is_trial:
- logger.warning(
- '🔁 Автопокупка трафика: у пользователя %s пробная подписка',
- _format_user_id(user),
- )
+ logger.warning('🔁 Автопокупка трафика: у пользователя пробная подписка', format_user_id=_format_user_id(user))
await user_cart_service.delete_user_cart(user.id)
return False
if subscription.traffic_limit_gb == 0:
logger.warning(
- '🔁 Автопокупка трафика: у пользователя %s уже безлимитный трафик',
- _format_user_id(user),
+ '🔁 Автопокупка трафика: у пользователя уже безлимитный трафик', format_user_id=_format_user_id(user)
)
await user_cart_service.delete_user_cart(user.id)
return False
@@ -1418,15 +1390,14 @@ async def _auto_add_traffic(
)
if not success:
logger.warning(
- '❌ Автопокупка трафика: не удалось списать баланс пользователя %s',
- _format_user_id(user),
+ '❌ Автопокупка трафика: не удалось списать баланс пользователя', format_user_id=_format_user_id(user)
)
return False
except Exception as error:
logger.error(
- '❌ Автопокупка трафика: ошибка списания баланса пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка трафика: ошибка списания баланса пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
@@ -1439,9 +1410,9 @@ async def _auto_add_traffic(
await db.refresh(subscription)
except Exception as error:
logger.error(
- '❌ Автопокупка трафика: ошибка добавления трафика пользователю %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка трафика: ошибка добавления трафика пользователю',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
await db.rollback()
@@ -1453,21 +1424,21 @@ async def _auto_add_traffic(
await subscription_service.update_remnawave_user(db, subscription)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка трафика: не удалось обновить Remnawave для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка трафика: не удалось обновить Remnawave для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Clear cart (transaction already created in subtract_user_balance)
await user_cart_service.delete_user_cart(user.id)
logger.info(
- '✅ Автопокупка трафика: пользователь %s добавил %s ГБ (было %s, стало %s) за %s коп.',
- _format_user_id(user),
- traffic_gb,
- old_traffic_limit,
- subscription.traffic_limit_gb,
- price_kopeks,
+ '✅ Автопокупка трафика: пользователь добавил ГБ (было , стало) за коп.',
+ format_user_id=_format_user_id(user),
+ traffic_gb=traffic_gb,
+ old_traffic_limit=old_traffic_limit,
+ traffic_limit_gb=subscription.traffic_limit_gb,
+ price_kopeks=price_kopeks,
)
# WebSocket notification for cabinet
@@ -1481,10 +1452,7 @@ async def _auto_add_traffic(
amount_kopeks=price_kopeks,
)
except Exception as ws_error:
- logger.warning(
- '⚠️ Автопокупка трафика: не удалось отправить WebSocket уведомление: %s',
- ws_error,
- )
+ logger.warning('⚠️ Автопокупка трафика: не удалось отправить WebSocket уведомление', ws_error=ws_error)
# User notification
if bot and user.telegram_id:
@@ -1529,9 +1497,7 @@ async def _auto_add_traffic(
)
except Exception as error:
logger.warning(
- '⚠️ Автопокупка трафика: не удалось уведомить пользователя %s: %s',
- user.telegram_id,
- error,
+ '⚠️ Автопокупка трафика: не удалось уведомить пользователя', telegram_id=user.telegram_id, error=error
)
# Admin notification
@@ -1548,10 +1514,7 @@ async def _auto_add_traffic(
price_kopeks,
)
except Exception as error:
- logger.warning(
- '⚠️ Автопокупка трафика: не удалось уведомить админов: %s',
- error,
- )
+ logger.warning('⚠️ Автопокупка трафика: не удалось уведомить админов', error=error)
return True
@@ -1582,7 +1545,7 @@ async def auto_purchase_saved_cart_after_topup(
if not cart_data:
return False
- logger.info('🔁 Автопокупка: обнаружена сохранённая корзина у пользователя %s', _format_user_id(user))
+ logger.info('🔁 Автопокупка: обнаружена сохранённая корзина у пользователя', format_user_id=_format_user_id(user))
cart_mode = cart_data.get('cart_mode') or cart_data.get('mode')
@@ -1599,18 +1562,18 @@ async def auto_purchase_saved_cart_after_topup(
and (datetime.utcnow() - last_tx.created_at) < timedelta(seconds=60)
):
logger.info(
- '🔁 Автопокупка: пропускаем для пользователя %s - подписка уже куплена %s секунд назад',
- _format_user_id(user),
- (datetime.utcnow() - last_tx.created_at).total_seconds(),
+ '🔁 Автопокупка: пропускаем для пользователя - подписка уже куплена секунд назад',
+ format_user_id=_format_user_id(user),
+ total_seconds=(datetime.utcnow() - last_tx.created_at).total_seconds(),
)
# Очищаем корзину чтобы не срабатывало повторно
await user_cart_service.delete_user_cart(user.id)
return False
except Exception as check_error:
logger.warning(
- '🔁 Автопокупка: ошибка проверки последней транзакции для %s: %s',
- _format_user_id(user),
- check_error,
+ '🔁 Автопокупка: ошибка проверки последней транзакции для',
+ format_user_id=_format_user_id(user),
+ check_error=check_error,
)
# Обработка продления подписки
@@ -1637,16 +1600,14 @@ async def auto_purchase_saved_cart_after_topup(
prepared = await _prepare_auto_purchase(db, user, cart_data)
except PurchaseValidationError as error:
logger.error(
- '❌ Автопокупка: ошибка валидации корзины пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка: ошибка валидации корзины пользователя', format_user_id=_format_user_id(user), error=error
)
return False
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '❌ Автопокупка: непредвиденная ошибка при подготовке корзины %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка: непредвиденная ошибка при подготовке корзины',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
@@ -1659,18 +1620,18 @@ async def auto_purchase_saved_cart_after_topup(
if pricing.final_total <= 0:
logger.warning(
- '❌ Автопокупка: итоговая сумма для пользователя %s некорректна (%s)',
- _format_user_id(user),
- pricing.final_total,
+ '❌ Автопокупка: итоговая сумма для пользователя некорректна',
+ format_user_id=_format_user_id(user),
+ final_total=pricing.final_total,
)
return False
if user.balance_kopeks < pricing.final_total:
logger.info(
- '🔁 Автопокупка: у пользователя %s недостаточно средств (%s < %s)',
- _format_user_id(user),
- user.balance_kopeks,
- pricing.final_total,
+ '🔁 Автопокупка: у пользователя недостаточно средств (<)',
+ format_user_id=_format_user_id(user),
+ balance_kopeks=user.balance_kopeks,
+ final_total=pricing.final_total,
)
return False
@@ -1684,22 +1645,21 @@ async def auto_purchase_saved_cart_after_topup(
)
except PurchaseBalanceError:
logger.info(
- '🔁 Автопокупка: баланс пользователя %s изменился и стал недостаточным',
- _format_user_id(user),
+ '🔁 Автопокупка: баланс пользователя изменился и стал недостаточным', format_user_id=_format_user_id(user)
)
return False
except PurchaseValidationError as error:
logger.error(
- '❌ Автопокупка: не удалось подтвердить корзину пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка: не удалось подтвердить корзину пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
)
return False
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '❌ Автопокупка: ошибка оформления подписки для пользователя %s: %s',
- _format_user_id(user),
- error,
+ '❌ Автопокупка: ошибка оформления подписки для пользователя',
+ format_user_id=_format_user_id(user),
+ error=error,
exc_info=True,
)
return False
@@ -1725,9 +1685,9 @@ async def auto_purchase_saved_cart_after_topup(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '⚠️ Автопокупка: не удалось отправить уведомление админам (%s): %s',
- _format_user_id(user),
- error,
+ '⚠️ Автопокупка: не удалось отправить уведомление админам',
+ format_user_id=_format_user_id(user),
+ error=error,
)
# Send user notification only for Telegram users
@@ -1777,15 +1737,15 @@ async def auto_purchase_saved_cart_after_topup(
)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- '⚠️ Автопокупка: не удалось уведомить пользователя %s: %s',
- user.telegram_id or user.id,
- error,
+ '⚠️ Автопокупка: не удалось уведомить пользователя',
+ telegram_id=user.telegram_id or user.id,
+ error=error,
)
logger.info(
- '✅ Автопокупка: подписка на %s дней оформлена для пользователя %s',
- selection.period.days,
- _format_user_id(user),
+ '✅ Автопокупка: подписка на дней оформлена для пользователя',
+ days=selection.period.days,
+ format_user_id=_format_user_id(user),
)
# Send WebSocket notification to cabinet frontend
@@ -1806,9 +1766,9 @@ async def auto_purchase_saved_cart_after_topup(
)
except Exception as ws_error:
logger.warning(
- '⚠️ Автопокупка: не удалось отправить WS уведомление для %s: %s',
- _format_user_id(user),
- ws_error,
+ '⚠️ Автопокупка: не удалось отправить WS уведомление для',
+ format_user_id=_format_user_id(user),
+ ws_error=ws_error,
)
return True
diff --git a/app/services/subscription_checkout_service.py b/app/services/subscription_checkout_service.py
index 6d2ad17d..1d7a7fcd 100644
--- a/app/services/subscription_checkout_service.py
+++ b/app/services/subscription_checkout_service.py
@@ -1,12 +1,11 @@
-import logging
-
+import structlog
from sqlalchemy.exc import MissingGreenlet
from app.database.models import Subscription, User
from app.utils.cache import UserCache
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
_CHECKOUT_SESSION_KEY = 'subscription_checkout'
@@ -57,9 +56,9 @@ def should_offer_checkout_resume(
subscription = getattr(user, 'subscription', None)
except MissingGreenlet as error:
logger.warning(
- 'Не удалось лениво загрузить подписку пользователя %s при проверке возврата к checkout: %s',
- getattr(user, 'id', None),
- error,
+ 'Не удалось лениво загрузить подписку пользователя при проверке возврата к checkout',
+ getattr=getattr(user, 'id', None),
+ error=error,
)
subscription = None
diff --git a/app/services/subscription_purchase_service.py b/app/services/subscription_purchase_service.py
index 20b389f1..d7c3b395 100644
--- a/app/services/subscription_purchase_service.py
+++ b/app/services/subscription_purchase_service.py
@@ -1,9 +1,9 @@
-import logging
from collections.abc import Sequence
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -35,7 +35,7 @@ from app.utils.promo_offer import get_user_active_promo_discount_percent
from app.utils.user_utils import mark_user_as_had_paid_subscription
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
@dataclass
@@ -349,7 +349,7 @@ class MiniAppSubscriptionPurchaseService:
try:
existing = await get_server_squad_by_uuid(db, uuid)
except Exception as error: # pragma: no cover - defensive logging
- logger.warning('Failed to load server squad %s: %s', uuid, error)
+ logger.warning('Failed to load server squad', uuid=uuid, error=error)
existing = None
if existing:
server_catalog[uuid] = existing
@@ -1053,9 +1053,9 @@ class MiniAppSubscriptionPurchaseService:
await db.refresh(subscription)
except Exception as refresh_error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to refresh existing subscription %s: %s',
- getattr(subscription, 'id', None),
- refresh_error,
+ 'Failed to refresh existing subscription',
+ getattr=getattr(subscription, 'id', None),
+ refresh_error=refresh_error,
)
else:
result = await db.execute(select(Subscription).where(Subscription.user_id == user.id))
@@ -1085,7 +1085,7 @@ class MiniAppSubscriptionPurchaseService:
first_paid_period_days=pricing.selection.period.days,
)
except Exception as conversion_error: # pragma: no cover - defensive logging
- logger.error('Failed to create subscription conversion record: %s', conversion_error)
+ logger.error('Failed to create subscription conversion record', conversion_error=conversion_error)
subscription.is_trial = False
subscription.status = SubscriptionStatus.ACTIVE.value
@@ -1128,7 +1128,7 @@ class MiniAppSubscriptionPurchaseService:
)
await add_user_to_servers(db, pricing.server_ids)
except Exception as error: # pragma: no cover - defensive logging
- logger.error('Failed to register subscription servers: %s', error)
+ logger.error('Failed to register subscription servers', error=error)
subscription_service = SubscriptionService()
# При покупке подписки ВСЕГДА сбрасываем трафик в панели
@@ -1148,7 +1148,7 @@ class MiniAppSubscriptionPurchaseService:
reset_reason='miniapp purchase',
)
except Exception as remnawave_error: # pragma: no cover - defensive logging
- logger.error('Failed to sync subscription with RemnaWave: %s', remnawave_error)
+ logger.error('Failed to sync subscription with RemnaWave', remnawave_error=remnawave_error)
transaction = await create_transaction(
db=db,
diff --git a/app/services/subscription_renewal_service.py b/app/services/subscription_renewal_service.py
index c8ec39b6..d55b05fd 100644
--- a/app/services/subscription_renewal_service.py
+++ b/app/services/subscription_renewal_service.py
@@ -2,13 +2,13 @@ from __future__ import annotations
import base64
import json
-import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from uuid import uuid4
+import structlog
from aiogram import Bot
from sqlalchemy.ext.asyncio import AsyncSession
@@ -33,7 +33,7 @@ from app.utils.pricing_utils import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class SubscriptionRenewalError(Exception):
@@ -300,7 +300,7 @@ async def with_admin_notification_service(
service = AdminNotificationService(bot)
await handler(service)
except Exception as error: # pragma: no cover - defensive logging
- logger.error('Failed to send admin notification from renewal service: %s', error)
+ logger.error('Failed to send admin notification from renewal service', error=error)
finally:
if bot is not None:
await bot.session.close()
@@ -371,9 +371,9 @@ class SubscriptionRenewalService:
discounted_total,
):
logger.warning(
- 'Renewal pricing validation failed for subscription %s (period %s)',
- subscription.id,
- period_days,
+ 'Renewal pricing validation failed for subscription (period)',
+ subscription_id=subscription.id,
+ period_days=period_days,
)
from app.utils.promo_offer import get_user_active_promo_discount_percent
@@ -467,10 +467,10 @@ class SubscriptionRenewalService:
)
except Exception as refund_error:
logger.critical(
- 'CRITICAL: Failed to refund %s kopeks to user %s after extension failure: %s',
- charge_from_balance,
- user.id,
- refund_error,
+ 'CRITICAL: Failed to refund kopeks to user after extension failure',
+ charge_from_balance=charge_from_balance,
+ user_id=user.id,
+ refund_error=refund_error,
)
raise
@@ -486,9 +486,9 @@ class SubscriptionRenewalService:
)
except Exception as error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to record renewal server prices for subscription %s: %s',
- subscription_after.id,
- error,
+ 'Failed to record renewal server prices for subscription',
+ subscription_after_id=subscription_after.id,
+ error=error,
)
subscription_service = SubscriptionService()
@@ -500,12 +500,12 @@ class SubscriptionRenewalService:
reset_reason='subscription renewal',
)
except RemnaWaveConfigurationError as error: # pragma: no cover - configuration issues
- logger.warning('RemnaWave update skipped: %s', error)
+ logger.warning('RemnaWave update skipped', error=error)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
- 'Failed to update RemnaWave user for subscription %s: %s',
- subscription_after.id,
- error,
+ 'Failed to update RemnaWave user for subscription',
+ subscription_after_id=subscription_after.id,
+ error=error,
)
transaction: Transaction | None = None
@@ -520,9 +520,9 @@ class SubscriptionRenewalService:
)
except Exception as error: # pragma: no cover - defensive logging
logger.warning(
- 'Failed to create renewal transaction for subscription %s: %s',
- subscription_after.id,
- error,
+ 'Failed to create renewal transaction for subscription',
+ subscription_after_id=subscription_after.id,
+ error=error,
)
await db.refresh(user)
@@ -584,10 +584,10 @@ class SubscriptionRenewalService:
if unavailable_servers:
user_identifier = user.telegram_id or user.email or f'user#{user.id}'
logger.warning(
- f'⚠️ Пользователь {user_identifier} (promo_group={user_promo_group.name}) '
- f'продлевает подписку с серверами, недоступными для его промогруппы: '
- f'{", ".join(unavailable_servers)}. '
- f'Это может привести к неправильному расчёту цены!'
+ '⚠️ Пользователь (promo_group=) продлевает подписку с серверами, недоступными для его промогруппы: . Это может привести к неправильному расчёту цены!',
+ user_identifier=user_identifier,
+ user_promo_group_name=user_promo_group.name,
+ value=', '.join(unavailable_servers),
)
def build_option_payload(
diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py
index c5f99548..95674be1 100644
--- a/app/services/subscription_service.py
+++ b/app/services/subscription_service.py
@@ -1,7 +1,7 @@
-import logging
from contextlib import asynccontextmanager
from datetime import datetime
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -17,7 +17,7 @@ from app.utils.subscription_utils import (
)
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _resolve_discount_percent(
@@ -79,14 +79,17 @@ def get_traffic_reset_strategy(tariff=None):
if tariff_mode is not None:
mapped_strategy = strategy_mapping.get(tariff_mode.upper(), 'NO_RESET')
logger.info(
- f"🔄 Стратегия сброса трафика из тарифа '{getattr(tariff, 'name', 'N/A')}': {tariff_mode} -> {mapped_strategy}"
+ '🔄 Стратегия сброса трафика из тарифа',
+ value=getattr(tariff, 'name', 'N/A'),
+ tariff_mode=tariff_mode,
+ mapped_strategy=mapped_strategy,
)
return getattr(TrafficLimitStrategy, mapped_strategy)
# Используем глобальную настройку
strategy = settings.DEFAULT_TRAFFIC_RESET_STRATEGY.upper()
mapped_strategy = strategy_mapping.get(strategy, 'NO_RESET')
- logger.info(f'🔄 Стратегия сброса трафика из конфига: {strategy} -> {mapped_strategy}')
+ logger.info('🔄 Стратегия сброса трафика из конфига', strategy=strategy, mapped_strategy=mapped_strategy)
return getattr(TrafficLimitStrategy, mapped_strategy)
@@ -141,7 +144,8 @@ class SubscriptionService:
if self._config_error:
logger.warning(
- 'RemnaWave API недоступен: %s. Подписочный сервис будет работать в оффлайн-режиме.', self._config_error
+ 'RemnaWave API недоступен: . Подписочный сервис будет работать в оффлайн-режиме.',
+ config_error=self._config_error,
)
self._last_config_signature = config_signature
@@ -184,12 +188,12 @@ class SubscriptionService:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
- logger.error(f'Пользователь {subscription.user_id} не найден')
+ logger.error('Пользователь не найден', user_id=subscription.user_id)
return None
validation_success = await self.validate_and_clean_subscription(db, subscription, user)
if not validation_success:
- logger.error(f'Ошибка валидации подписки для пользователя {self._format_user_log(user)}')
+ logger.error('Ошибка валидации подписки для пользователя', _format_user_log=self._format_user_log(user))
return None
# Загружаем tariff заранее, чтобы избежать lazy loading в async контексте
@@ -224,14 +228,16 @@ class SubscriptionService:
pass
if existing_users:
- logger.info(f'🔄 Найден существующий пользователь в панели для {self._format_user_log(user)}')
+ logger.info(
+ '🔄 Найден существующий пользователь в панели для', _format_user_log=self._format_user_log(user)
+ )
remnawave_user = existing_users[0]
try:
await api.reset_user_devices(remnawave_user.uuid)
- logger.info(f'🔧 Сброшены HWID устройства для {self._format_user_log(user)}')
+ logger.info('🔧 Сброшены HWID устройства для', _format_user_log=self._format_user_log(user))
except Exception as hwid_error:
- logger.warning(f'⚠️ Не удалось сбросить HWID: {hwid_error}')
+ logger.warning('⚠️ Не удалось сбросить HWID', hwid_error=hwid_error)
update_kwargs = dict(
uuid=remnawave_user.uuid,
@@ -267,7 +273,9 @@ class SubscriptionService:
)
else:
- logger.info(f'🆕 Создаем нового пользователя в панели для {self._format_user_log(user)}')
+ logger.info(
+ '🆕 Создаем нового пользователя в панели для', _format_user_log=self._format_user_log(user)
+ )
username = settings.format_remnawave_username(
full_name=user.full_name,
username=user.username,
@@ -316,17 +324,17 @@ class SubscriptionService:
await db.commit()
- logger.info(f'✅ Создан/обновлен RemnaWave пользователь для подписки {subscription.id}')
- logger.info(f'🔗 Ссылка на подписку: {updated_user.subscription_url}')
+ logger.info('✅ Создан/обновлен RemnaWave пользователь для подписки', subscription_id=subscription.id)
+ logger.info('🔗 Ссылка на подписку', subscription_url=updated_user.subscription_url)
strategy_name = settings.DEFAULT_TRAFFIC_RESET_STRATEGY
- logger.info(f'📊 Стратегия сброса трафика: {strategy_name}')
+ logger.info('📊 Стратегия сброса трафика', strategy_name=strategy_name)
return updated_user
except RemnaWaveAPIError as e:
- logger.error(f'Ошибка RemnaWave API: {e}')
+ logger.error('Ошибка RemnaWave API', error=e)
return None
except Exception as e:
- logger.error(f'Ошибка создания RemnaWave пользователя: {e}')
+ logger.error('Ошибка создания RemnaWave пользователя', error=e)
return None
async def update_remnawave_user(
@@ -340,7 +348,7 @@ class SubscriptionService:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user or not user.remnawave_uuid:
- logger.error(f'RemnaWave UUID не найден для пользователя {subscription.user_id}')
+ logger.error('RemnaWave UUID не найден для пользователя', user_id=subscription.user_id)
return None
# Загружаем tariff заранее, чтобы избежать lazy loading в async контексте
@@ -359,9 +367,10 @@ class SubscriptionService:
# Логируем если статус и end_date не согласованы (для отладки)
if subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date <= current_time:
logger.warning(
- f'⚠️ update_remnawave_user: подписка {subscription.id} имеет статус ACTIVE, '
- f'но end_date ({subscription.end_date}) <= now ({current_time}). '
- f'Отправляем в RemnaWave как EXPIRED, но НЕ меняем статус в БД.'
+ '⚠️ update_remnawave_user: подписка имеет статус ACTIVE, но end_date <= now . Отправляем в RemnaWave как EXPIRED, но НЕ меняем статус в БД.',
+ subscription_id=subscription.id,
+ end_date=subscription.end_date,
+ current_time=current_time,
)
user_tag = self._resolve_user_tag(subscription)
@@ -407,16 +416,20 @@ class SubscriptionService:
await db.commit()
status_text = 'активным' if is_actually_active else 'истёкшим'
- logger.info(f'✅ Обновлен RemnaWave пользователь {user.remnawave_uuid} со статусом {status_text}')
+ logger.info(
+ '✅ Обновлен RemnaWave пользователь со статусом',
+ remnawave_uuid=user.remnawave_uuid,
+ status_text=status_text,
+ )
strategy_name = settings.DEFAULT_TRAFFIC_RESET_STRATEGY
- logger.info(f'📊 Стратегия сброса трафика: {strategy_name}')
+ logger.info('📊 Стратегия сброса трафика', strategy_name=strategy_name)
return updated_user
except RemnaWaveAPIError as e:
- logger.error(f'Ошибка RemnaWave API: {e}')
+ logger.error('Ошибка RemnaWave API', error=e)
return None
except Exception as e:
- logger.error(f'Ошибка обновления RemnaWave пользователя: {e}')
+ logger.error('Ошибка обновления RemnaWave пользователя', error=e)
return None
@staticmethod
@@ -441,24 +454,28 @@ class SubscriptionService:
try:
await api.reset_user_traffic(user_uuid)
reason_text = f' ({reset_reason})' if reset_reason else ''
- logger.info(f'🔄 Сброшен трафик RemnaWave для {self._format_user_log(user)}{reason_text}')
+ logger.info(
+ '🔄 Сброшен трафик RemnaWave для', _format_user_log=self._format_user_log(user), reason_text=reason_text
+ )
except Exception as exc:
- logger.warning(f'⚠️ Не удалось сбросить трафик RemnaWave для {self._format_user_log(user)}: {exc}')
+ logger.warning(
+ '⚠️ Не удалось сбросить трафик RemnaWave для', _format_user_log=self._format_user_log(user), error=exc
+ )
async def disable_remnawave_user(self, user_uuid: str) -> bool:
try:
async with self.get_api_client() as api:
await api.disable_user(user_uuid)
- logger.info(f'✅ Отключен RemnaWave пользователь {user_uuid}')
+ logger.info('✅ Отключен RemnaWave пользователь', user_uuid=user_uuid)
return True
except Exception as e:
error_msg = str(e).lower()
# "User already disabled" - считаем успехом
if 'already disabled' in error_msg:
- logger.info(f'✅ RemnaWave пользователь {user_uuid} уже отключен')
+ logger.info('✅ RemnaWave пользователь уже отключен', user_uuid=user_uuid)
return True
- logger.error(f'Ошибка отключения RemnaWave пользователя: {e}')
+ logger.error('Ошибка отключения RemnaWave пользователя', error=e)
return False
async def enable_remnawave_user(self, user_uuid: str) -> bool:
@@ -466,16 +483,16 @@ class SubscriptionService:
try:
async with self.get_api_client() as api:
await api.enable_user(user_uuid)
- logger.info(f'✅ Включен RemnaWave пользователь {user_uuid}')
+ logger.info('✅ Включен RemnaWave пользователь', user_uuid=user_uuid)
return True
except Exception as e:
error_msg = str(e).lower()
# "User already enabled" - считаем успехом
if 'already enabled' in error_msg:
- logger.info(f'✅ RemnaWave пользователь {user_uuid} уже включен')
+ logger.info('✅ RemnaWave пользователь уже включен', user_uuid=user_uuid)
return True
- logger.error(f'Ошибка включения RemnaWave пользователя: {e}')
+ logger.error('Ошибка включения RemnaWave пользователя', error=e)
return False
async def get_remnawave_squads(self) -> list[dict] | None:
@@ -492,11 +509,11 @@ class SubscriptionService:
'name': squad.name,
}
)
- logger.info(f'✅ Получено {len(result)} серверов из RemnaWave')
+ logger.info('✅ Получено серверов из RemnaWave', result_count=len(result))
return result
except Exception as e:
- logger.error(f'Ошибка получения серверов из RemnaWave: {e}')
+ logger.error('Ошибка получения серверов из RemnaWave', error=e)
return None
async def revoke_subscription(self, db: AsyncSession, subscription: Subscription) -> str | None:
@@ -513,11 +530,11 @@ class SubscriptionService:
subscription.subscription_crypto_link = updated_user.happ_crypto_link
await db.commit()
- logger.info(f'✅ Обновлена ссылка подписки для {self._format_user_log(user)}')
+ logger.info('✅ Обновлена ссылка подписки для', _format_user_log=self._format_user_log(user))
return updated_user.subscription_url
except Exception as e:
- logger.error(f'Ошибка обновления ссылки подписки: {e}')
+ logger.error('Ошибка обновления ссылки подписки', error=e)
return None
async def get_subscription_info(self, short_uuid: str) -> dict | None:
@@ -527,7 +544,7 @@ class SubscriptionService:
return info
except Exception as e:
- logger.error(f'Ошибка получения информации о подписке: {e}')
+ logger.error('Ошибка получения информации о подписке', error=e)
return None
async def sync_subscription_usage(self, db: AsyncSession, subscription: Subscription) -> bool:
@@ -546,11 +563,11 @@ class SubscriptionService:
await db.commit()
- logger.debug(f'Синхронизирован трафик для подписки {subscription.id}: {used_gb} ГБ')
+ logger.debug('Синхронизирован трафик для подписки ГБ', subscription_id=subscription.id, used_gb=used_gb)
return True
except Exception as e:
- logger.error(f'Ошибка синхронизации трафика: {e}')
+ logger.error('Ошибка синхронизации трафика', error=e)
return False
async def ensure_subscription_synced(
@@ -570,7 +587,7 @@ class SubscriptionService:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
- logger.error(f'Пользователь не найден для подписки {subscription.id}')
+ logger.error('Пользователь не найден для подписки', subscription_id=subscription.id)
return False, 'user_not_found'
# Проверяем, нужна ли синхронизация
@@ -584,19 +601,21 @@ class SubscriptionService:
if not remnawave_user:
needs_sync = True
logger.warning(
- f'Пользователь {user.remnawave_uuid} не найден в RemnaWave, требуется синхронизация'
+ 'Пользователь не найден в RemnaWave, требуется синхронизация',
+ remnawave_uuid=user.remnawave_uuid,
)
except Exception as check_error:
- logger.warning(f'Не удалось проверить пользователя в RemnaWave: {check_error}')
+ logger.warning('Не удалось проверить пользователя в RemnaWave', check_error=check_error)
# Продолжаем, возможно проблема временная
if not needs_sync:
return True, None
logger.info(
- f'Синхронизация подписки {subscription.id} с RemnaWave '
- f'(subscription_url={bool(subscription.subscription_url)}, '
- f'remnawave_uuid={bool(user.remnawave_uuid)})'
+ 'Синхронизация подписки с RemnaWave (subscription_url=, remnawave_uuid=)',
+ subscription_id=subscription.id,
+ subscription_url=bool(subscription.subscription_url),
+ remnawave_uuid=bool(user.remnawave_uuid),
)
# Пытаемся синхронизировать
@@ -611,7 +630,8 @@ class SubscriptionService:
# Если update не удался (пользователь удалён из RemnaWave) — пробуем создать
if not result:
logger.warning(
- f'Не удалось обновить пользователя {user.remnawave_uuid} в RemnaWave, пробуем создать заново'
+ 'Не удалось обновить пользователя в RemnaWave, пробуем создать заново',
+ remnawave_uuid=user.remnawave_uuid,
)
# Сбрасываем старый UUID, create_remnawave_user установит новый
user.remnawave_uuid = None
@@ -632,18 +652,21 @@ class SubscriptionService:
await db.refresh(subscription)
await db.refresh(user)
logger.info(
- f'Подписка {subscription.id} успешно синхронизирована с RemnaWave. '
- f'URL: {subscription.subscription_url}'
+ 'Подписка успешно синхронизирована с RemnaWave. URL',
+ subscription_id=subscription.id,
+ subscription_url=subscription.subscription_url,
)
return True, None
- logger.error(f'Не удалось синхронизировать подписку {subscription.id} с RemnaWave')
+ logger.error('Не удалось синхронизировать подписку с RemnaWave', subscription_id=subscription.id)
return False, 'sync_failed'
except RemnaWaveAPIError as api_error:
- logger.error(f'Ошибка RemnaWave API при синхронизации подписки {subscription.id}: {api_error}')
+ logger.error(
+ 'Ошибка RemnaWave API при синхронизации подписки', subscription_id=subscription.id, api_error=api_error
+ )
return False, 'api_error'
except Exception as e:
- logger.error(f'Ошибка синхронизации подписки {subscription.id}: {e}')
+ logger.error('Ошибка синхронизации подписки', subscription_id=subscription.id, error=e)
return False, 'unknown_error'
async def calculate_subscription_price(
@@ -708,7 +731,7 @@ class SubscriptionService:
logger.debug(log_message)
else:
server_prices.append(0)
- logger.warning(f'Сервер ID {server_id} недоступен')
+ logger.warning('Сервер ID недоступен', server_id=server_id)
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
devices_discount_percent = _resolve_discount_percent(
@@ -742,7 +765,7 @@ class SubscriptionService:
if devices_discount > 0:
message += f' (скидка {devices_discount_percent}%: -{devices_discount / 100}₽ → {discounted_devices_price / 100}₽)'
logger.debug(message)
- logger.debug(f' ИТОГО: {total_price / 100}₽')
+ logger.debug('ИТОГО: ₽', total_price=total_price / 100)
return total_price, server_prices
@@ -826,7 +849,9 @@ class SubscriptionService:
total_price = base_price + discounted_servers_price + discounted_devices_price + discounted_traffic_price
- logger.debug(f'💰 Расчет стоимости продления для подписки {subscription.id} (по текущим ценам):')
+ logger.debug(
+ '💰 Расчет стоимости продления для подписки (по текущим ценам)', subscription_id=subscription.id
+ )
base_log = f' 📅 Период {period_days} дней: {base_price_original / 100}₽'
if base_discount_total > 0:
base_log += f' → {base_price / 100}₽ (скидка {period_discount_percent}%: -{base_discount_total / 100}₽)'
@@ -852,12 +877,12 @@ class SubscriptionService:
f' (скидка {traffic_discount_percent}%: -{traffic_discount / 100}₽ от {traffic_price / 100}₽)'
)
logger.debug(message)
- logger.debug(f' 💎 ИТОГО: {total_price / 100}₽')
+ logger.debug('💎 ИТОГО: ₽', total_price=total_price / 100)
return total_price
except Exception as e:
- logger.error(f'Ошибка расчета стоимости продления: {e}')
+ logger.error('Ошибка расчета стоимости продления', error=e)
from app.config import PERIOD_PRICES
return PERIOD_PRICES.get(period_days, 0)
@@ -874,7 +899,9 @@ class SubscriptionService:
if not remnawave_user:
logger.warning(
- f'⚠️ Пользователь {user_log} имеет UUID {user.remnawave_uuid}, но не найден в панели'
+ '⚠️ Пользователь имеет UUID но не найден в панели',
+ user_log=user_log,
+ remnawave_uuid=user.remnawave_uuid,
)
needs_cleanup = True
# Проверяем telegram_id только если он задан у обоих
@@ -884,11 +911,13 @@ class SubscriptionService:
and remnawave_user.telegram_id != user.telegram_id
):
logger.warning(
- f'⚠️ Несоответствие telegram_id для {user_log}: panel={remnawave_user.telegram_id}'
+ '⚠️ Несоответствие telegram_id для panel',
+ user_log=user_log,
+ telegram_id=remnawave_user.telegram_id,
)
needs_cleanup = True
except Exception as api_error:
- logger.error(f'❌ Ошибка проверки пользователя в панели: {api_error}')
+ logger.error('❌ Ошибка проверки пользователя в панели', api_error=api_error)
needs_cleanup = True
if subscription.remnawave_short_uuid and not user.remnawave_uuid:
@@ -896,7 +925,7 @@ class SubscriptionService:
needs_cleanup = True
if needs_cleanup:
- logger.info(f'🧹 Очищаем мусорные данные подписки для {user_log}')
+ logger.info('🧹 Очищаем мусорные данные подписки для', user_log=user_log)
subscription.remnawave_short_uuid = None
subscription.subscription_url = ''
@@ -906,12 +935,12 @@ class SubscriptionService:
user.remnawave_uuid = None
await db.commit()
- logger.info(f'✅ Мусорные данные очищены для {user_log}')
+ logger.info('✅ Мусорные данные очищены для', user_log=user_log)
return True
except Exception as e:
- logger.error(f'❌ Ошибка валидации подписки для {self._format_user_log(user)}: {e}')
+ logger.error('❌ Ошибка валидации подписки для', _format_user_log=self._format_user_log(user), error=e)
await db.rollback()
return False
@@ -939,20 +968,22 @@ class SubscriptionService:
price = server.price_kopeks
total_price += price
prices_list.append(price)
- logger.debug(f'🏷️ Страна {server.display_name}: {price / 100}₽')
+ logger.debug('🏷️ Страна ₽', display_name=server.display_name, price=price / 100)
else:
default_price = 0
total_price += default_price
prices_list.append(default_price)
logger.warning(
- f'⚠️ Сервер {country_uuid} недоступен, используем базовую цену: {default_price / 100}₽'
+ '⚠️ Сервер недоступен, используем базовую цену: ₽',
+ country_uuid=country_uuid,
+ default_price=default_price / 100,
)
- logger.info(f'💰 Общая стоимость стран: {total_price / 100}₽')
+ logger.info('💰 Общая стоимость стран: ₽', total_price=total_price / 100)
return total_price, prices_list
except Exception as e:
- logger.error(f'Ошибка получения цен стран: {e}')
+ logger.error('Ошибка получения цен стран', error=e)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
@@ -961,7 +992,7 @@ class SubscriptionService:
total_price, _ = await self.get_countries_price_by_uuids(country_uuids, db)
return total_price
except Exception as e:
- logger.error(f'Ошибка получения цен стран: {e}')
+ logger.error('Ошибка получения цен стран', error=e)
return len(country_uuids) * 1000
async def calculate_subscription_price_with_months(
@@ -1032,7 +1063,7 @@ class SubscriptionService:
logger.debug(log_message)
else:
server_prices.append(0)
- logger.warning(f'Сервер ID {server_id} недоступен')
+ logger.warning('Сервер ID недоступен', server_id=server_id)
additional_devices = max(0, devices - settings.DEFAULT_DEVICE_LIMIT)
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
@@ -1048,7 +1079,9 @@ class SubscriptionService:
total_price = base_price + total_traffic_price + total_servers_price + total_devices_price
- logger.debug(f'Расчет стоимости новой подписки на {period_days} дней ({months_in_period} мес):')
+ logger.debug(
+ 'Расчет стоимости новой подписки на дней ( мес)', period_days=period_days, months_in_period=months_in_period
+ )
base_log = f' Период {period_days} дней: {base_price_original / 100}₽'
if base_discount_total > 0:
base_log += f' → {base_price / 100}₽ (скидка {period_discount_percent}%: -{base_discount_total / 100}₽)'
@@ -1072,7 +1105,7 @@ class SubscriptionService:
f' (скидка {devices_discount_percent}%: -{devices_discount_per_month * months_in_period / 100}₽)'
)
logger.debug(message)
- logger.debug(f' ИТОГО: {total_price / 100}₽')
+ logger.debug('ИТОГО: ₽', total_price=total_price / 100)
return total_price, server_prices
@@ -1162,7 +1195,10 @@ class SubscriptionService:
total_price = base_price + total_servers_price + total_devices_price + total_traffic_price
logger.debug(
- f'💰 Расчет стоимости продления подписки {subscription.id} на {period_days} дней ({months_in_period} мес):'
+ '💰 Расчет стоимости продления подписки на дней ( мес)',
+ subscription_id=subscription.id,
+ period_days=period_days,
+ months_in_period=months_in_period,
)
base_log = f' 📅 Период {period_days} дней: {base_price_original / 100}₽'
if base_discount_total > 0:
@@ -1183,12 +1219,12 @@ class SubscriptionService:
if traffic_discount_per_month > 0:
message += f' (скидка {traffic_discount_percent}%: -{traffic_discount_per_month * months_in_period / 100}₽)'
logger.debug(message)
- logger.debug(f' 💎 ИТОГО: {total_price / 100}₽')
+ logger.debug('💎 ИТОГО: ₽', total_price=total_price / 100)
return total_price
except Exception as e:
- logger.error(f'Ошибка расчета стоимости продления: {e}')
+ logger.error('Ошибка расчета стоимости продления', error=e)
from app.config import PERIOD_PRICES
return PERIOD_PRICES.get(period_days, 0)
@@ -1284,7 +1320,7 @@ class SubscriptionService:
)
logger.info(message)
- logger.info(f'Итого доплата за {months_to_pay} мес: {total_price / 100}₽')
+ logger.info('Итого доплата за мес: ₽', months_to_pay=months_to_pay, total_price=total_price / 100)
return total_price
def _gb_to_bytes(self, gb: int | None) -> int:
diff --git a/app/services/support_settings_service.py b/app/services/support_settings_service.py
index d948f255..774cb0bf 100644
--- a/app/services/support_settings_service.py
+++ b/app/services/support_settings_service.py
@@ -1,11 +1,12 @@
import json
-import logging
from pathlib import Path
+import structlog
+
from app.config import settings
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class SupportSettingsService:
@@ -20,7 +21,7 @@ class SupportSettingsService:
try:
cls._storage_path.parent.mkdir(parents=True, exist_ok=True)
except Exception as e:
- logger.error(f'Failed to ensure settings dir: {e}')
+ logger.error('Failed to ensure settings dir', error=e)
@classmethod
def _load(cls) -> None:
@@ -33,7 +34,7 @@ class SupportSettingsService:
else:
cls._data = {}
except Exception as e:
- logger.error(f'Failed to load support settings: {e}')
+ logger.error('Failed to load support settings', error=e)
cls._data = {}
cls._loaded = True
@@ -44,7 +45,7 @@ class SupportSettingsService:
cls._storage_path.write_text(json.dumps(cls._data, ensure_ascii=False, indent=2), encoding='utf-8')
return True
except Exception as e:
- logger.error(f'Failed to save support settings: {e}')
+ logger.error('Failed to save support settings', error=e)
return False
# Mode
diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py
index eef645fc..3e6a6164 100644
--- a/app/services/system_settings_service.py
+++ b/app/services/system_settings_service.py
@@ -1,9 +1,9 @@
import hashlib
import json
-import logging
from dataclasses import dataclass
from typing import Any, Optional, Union, get_args, get_origin
+import structlog
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -23,7 +23,7 @@ from app.database.models import SystemSetting
from app.database.universal_migration import ensure_default_web_api_token
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
def _title_from_key(key: str) -> str:
@@ -1226,7 +1226,7 @@ class BotConfigurationService:
if isinstance(price_value, int):
label = f'{label} — {settings.format_price(price_value)}'
except Exception:
- logger.debug('Не удалось форматировать цену для периода %s', days, exc_info=True)
+ logger.debug('Не удалось форматировать цену для периода', days=days, exc_info=True)
options.append(ChoiceOption(days, label))
@@ -1269,7 +1269,7 @@ class BotConfigurationService:
try:
packages = settings.get_traffic_packages()
except Exception as error:
- logger.warning('Не удалось получить пакеты трафика: %s', error, exc_info=True)
+ logger.warning('Не удалось получить пакеты трафика', error=error, exc_info=True)
packages = []
traffic_values: set[int] = {0}
@@ -1429,15 +1429,12 @@ class BotConfigurationService:
for key, raw_value in overrides.items():
if cls._is_env_override(key):
- logger.debug(
- 'Пропускаем настройку %s из БД: используется значение из окружения',
- key,
- )
+ logger.debug('Пропускаем настройку из БД: используется значение из окружения', key=key)
continue
try:
parsed_value = cls.deserialize_value(key, raw_value)
except Exception as error:
- logger.error('Не удалось применить настройку %s: %s', key, error)
+ logger.error('Не удалось применить настройку', key=key, error=error)
continue
cls._overrides_raw[key] = raw_value
@@ -1548,10 +1545,7 @@ class BotConfigurationService:
raw_value = cls.serialize_value(key, value)
await upsert_system_setting(db, key, raw_value)
if cls._is_env_override(key):
- logger.info(
- 'Настройка %s сохранена в БД, но не применена: значение задаётся через окружение',
- key,
- )
+ logger.info('Настройка сохранена в БД, но не применена: значение задаётся через окружение', key=key)
cls._overrides_raw.pop(key, None)
else:
cls._overrides_raw[key] = raw_value
@@ -1574,10 +1568,7 @@ class BotConfigurationService:
await delete_system_setting(db, key)
cls._overrides_raw.pop(key, None)
if cls._is_env_override(key):
- logger.info(
- 'Настройка %s сброшена в БД, используется значение из окружения',
- key,
- )
+ logger.info('Настройка сброшена в БД, используется значение из окружения', key=key)
else:
original = cls.get_original_value(key)
cls._apply_to_settings(key, original)
@@ -1588,10 +1579,7 @@ class BotConfigurationService:
@classmethod
def _apply_to_settings(cls, key: str, value: Any) -> None:
if cls._is_env_override(key):
- logger.debug(
- 'Пропуск применения настройки %s: значение задано через окружение',
- key,
- )
+ logger.debug('Пропуск применения настройки : значение задано через окружение', key=key)
return
try:
setattr(settings, key, value)
@@ -1614,10 +1602,7 @@ class BotConfigurationService:
run_immediately=(key == 'REMNAWAVE_AUTO_SYNC_ENABLED' and bool(value))
)
except Exception as error:
- logger.error(
- 'Не удалось обновить сервис автосинхронизации RemnaWave: %s',
- error,
- )
+ logger.error('Не удалось обновить сервис автосинхронизации RemnaWave', error=error)
elif key in {
'REMNAWAVE_API_URL',
'REMNAWAVE_API_KEY',
@@ -1631,12 +1616,9 @@ class BotConfigurationService:
remnawave_sync_service.refresh_configuration()
except Exception as error:
- logger.error(
- 'Не удалось обновить конфигурацию сервиса автосинхронизации RemnaWave: %s',
- error,
- )
+ logger.error('Не удалось обновить конфигурацию сервиса автосинхронизации RemnaWave', error=error)
except Exception as error:
- logger.error('Не удалось применить значение %s=%s: %s', key, value, error)
+ logger.error('Не удалось применить значение', key=key, setting_value=value, error=error)
@staticmethod
async def _sync_default_web_api_token() -> None:
diff --git a/app/services/traffic_monitoring_service.py b/app/services/traffic_monitoring_service.py
index 37796043..b760b4e7 100644
--- a/app/services/traffic_monitoring_service.py
+++ b/app/services/traffic_monitoring_service.py
@@ -4,10 +4,10 @@
"""
import asyncio
-import logging
from dataclasses import dataclass
from datetime import datetime, time, timedelta
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -18,7 +18,7 @@ from app.services.remnawave_service import RemnaWaveService
from app.utils.cache import cache, cache_key
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
# Ключи для хранения snapshot в Redis
TRAFFIC_SNAPSHOT_KEY = 'traffic:snapshot'
@@ -122,12 +122,16 @@ class TrafficMonitoringServiceV2:
if success:
# Сохраняем время создания snapshot
await cache.set(TRAFFIC_SNAPSHOT_TIME_KEY, datetime.utcnow().isoformat(), expire=ttl)
- logger.info(f'📦 Snapshot сохранён в Redis: {len(snapshot)} пользователей, TTL {ttl // 3600}ч')
+ logger.info(
+ '📦 Snapshot сохранён в Redis: пользователей, TTL ч',
+ snapshot_count=len(snapshot),
+ value=ttl // 3600,
+ )
else:
logger.warning('⚠️ Не удалось сохранить snapshot в Redis')
return success
except Exception as e:
- logger.error(f'❌ Ошибка сохранения snapshot в Redis: {e}')
+ logger.error('❌ Ошибка сохранения snapshot в Redis', error=e)
return False
async def _load_snapshot_from_redis(self) -> dict[str, float] | None:
@@ -138,11 +142,11 @@ class TrafficMonitoringServiceV2:
if snapshot_data is not None and isinstance(snapshot_data, dict):
# Конвертируем обратно в float
result = {uuid: float(bytes_val) for uuid, bytes_val in snapshot_data.items()}
- logger.debug(f'📦 Snapshot загружен из Redis: {len(result)} пользователей')
+ logger.debug('📦 Snapshot загружен из Redis: пользователей', result_count=len(result))
return result
return None
except Exception as e:
- logger.error(f'❌ Ошибка загрузки snapshot из Redis: {e}')
+ logger.error('❌ Ошибка загрузки snapshot из Redis', error=e)
return None
async def _get_snapshot_time_from_redis(self) -> datetime | None:
@@ -153,7 +157,7 @@ class TrafficMonitoringServiceV2:
return datetime.fromisoformat(time_str)
return None
except Exception as e:
- logger.error(f'❌ Ошибка получения времени snapshot: {e}')
+ logger.error('❌ Ошибка получения времени snapshot', error=e)
return None
async def _save_notification_to_redis(self, user_uuid: str) -> bool:
@@ -163,7 +167,7 @@ class TrafficMonitoringServiceV2:
ttl = 24 * 3600 # 24 часа
return await cache.set(key, datetime.utcnow().isoformat(), expire=ttl)
except Exception as e:
- logger.error(f'❌ Ошибка сохранения уведомления в Redis: {e}')
+ logger.error('❌ Ошибка сохранения уведомления в Redis', error=e)
return False
async def _get_notification_time_from_redis(self, user_uuid: str) -> datetime | None:
@@ -175,7 +179,7 @@ class TrafficMonitoringServiceV2:
return datetime.fromisoformat(time_str)
return None
except Exception as e:
- logger.error(f'❌ Ошибка получения времени уведомления: {e}')
+ logger.error('❌ Ошибка получения времени уведомления', error=e)
return None
# ============== Работа с нодами ==============
@@ -185,9 +189,9 @@ class TrafficMonitoringServiceV2:
try:
nodes = await self.remnawave_service.get_all_nodes()
self._nodes_cache = {node['uuid']: node['name'] for node in nodes if node.get('uuid') and node.get('name')}
- logger.debug(f'📋 Загружено {len(self._nodes_cache)} нод в кеш')
+ logger.debug('📋 Загружено нод в кеш', _nodes_cache_count=len(self._nodes_cache))
except Exception as e:
- logger.error(f'❌ Ошибка загрузки нод в кеш: {e}')
+ logger.error('❌ Ошибка загрузки нод в кеш', error=e)
def get_node_name(self, node_uuid: str | None) -> str | None:
"""Возвращает название ноды по UUID из кеша"""
@@ -249,7 +253,7 @@ class TrafficMonitoringServiceV2:
for uuid in expired:
del self._memory_notification_cache[uuid]
if expired:
- logger.debug(f'🧹 Очищено {len(expired)} записей из памяти уведомлений о трафике')
+ logger.debug('🧹 Очищено записей из памяти уведомлений о трафике', expired_count=len(expired))
# ============== Получение пользователей ==============
@@ -272,18 +276,18 @@ class TrafficMonitoringServiceV2:
break
all_users.extend(users)
- logger.debug(f'📊 Загружено {len(all_users)} пользователей...')
+ logger.debug('📊 Загружено пользователей...', all_users_count=len(all_users))
if len(users) < batch_size:
break
offset += batch_size
- logger.info(f'✅ Всего загружено {len(all_users)} пользователей из Remnawave')
+ logger.info('✅ Всего загружено пользователей из Remnawave', all_users_count=len(all_users))
return all_users
except Exception as e:
- logger.error(f'❌ Ошибка при получении пользователей: {e}')
+ logger.error('❌ Ошибка при получении пользователей', error=e)
return []
# ============== Быстрая проверка ==============
@@ -349,8 +353,9 @@ class TrafficMonitoringServiceV2:
if existing_snapshot is not None:
age = await self.get_snapshot_age_minutes()
logger.info(
- f'📦 Найден существующий snapshot в Redis: {len(existing_snapshot)} пользователей, '
- f'возраст {age:.1f} мин'
+ '📦 Найден существующий snapshot в Redis: пользователей, возраст мин',
+ existing_snapshot_count=len(existing_snapshot),
+ age=round(age, 1),
)
return len(existing_snapshot)
@@ -373,13 +378,15 @@ class TrafficMonitoringServiceV2:
new_snapshot[user.uuid] = current_bytes
except Exception as e:
- logger.error(f'❌ Ошибка при создании snapshot для {user.uuid}: {e}')
+ logger.error('❌ Ошибка при создании snapshot для', uuid=user.uuid, error=e)
# Сохраняем в Redis (с fallback на память)
await self._save_snapshot(new_snapshot)
elapsed = (datetime.utcnow() - start_time).total_seconds()
- logger.info(f'✅ Snapshot создан за {elapsed:.1f}с: {len(new_snapshot)} пользователей')
+ logger.info(
+ '✅ Snapshot создан за с: пользователей', elapsed=round(elapsed, 1), new_snapshot_count=len(new_snapshot)
+ )
return len(new_snapshot)
@@ -407,21 +414,23 @@ class TrafficMonitoringServiceV2:
excluded_user_uuids = self.get_excluded_user_uuids()
if monitored_nodes:
- logger.info(f'🔍 Мониторим только ноды: {monitored_nodes}')
+ logger.info('🔍 Мониторим только ноды', monitored_nodes=monitored_nodes)
elif ignored_nodes:
- logger.info(f'🚫 Игнорируем ноды: {ignored_nodes}')
+ logger.info('🚫 Игнорируем ноды', ignored_nodes=ignored_nodes)
else:
logger.info('📊 Мониторим все ноды')
if excluded_user_uuids:
- logger.info(f'🚫 Исключены пользователи: {excluded_user_uuids}')
+ logger.info('🚫 Исключены пользователи', excluded_user_uuids=excluded_user_uuids)
if is_first_run:
logger.info('🚀 Первый запуск быстрой проверки — создаём snapshot...')
else:
age = await self.get_snapshot_age_minutes()
logger.info(
- f'🚀 Быстрая проверка трафика (snapshot {age:.1f} мин назад, порог {self.get_fast_check_threshold_gb()} ГБ)...'
+ '🚀 Быстрая проверка трафика (snapshot мин назад, порог ГБ)...',
+ age=round(age, 1),
+ get_fast_check_threshold_gb=self.get_fast_check_threshold_gb(),
)
violations: list[TrafficViolation] = []
@@ -432,7 +441,11 @@ class TrafficMonitoringServiceV2:
# Загружаем предыдущий snapshot (из Redis или памяти)
previous_snapshot = await self._get_current_snapshot()
- logger.info(f'📦 Предыдущий snapshot: {len(previous_snapshot)} пользователей (is_first_run={is_first_run})')
+ logger.info(
+ '📦 Предыдущий snapshot: пользователей (is_first_run=)',
+ previous_snapshot_count=len(previous_snapshot),
+ is_first_run=is_first_run,
+ )
users_with_delta = 0
@@ -455,7 +468,7 @@ class TrafficMonitoringServiceV2:
# Пользователя не было в предыдущем snapshot — пропускаем (новый пользователь)
if user.uuid not in previous_snapshot:
- logger.debug(f'Пользователь {user.uuid[:8]} не найден в предыдущем snapshot, пропускаем')
+ logger.debug('Пользователь не найден в предыдущем snapshot, пропускаем', uuid=user.uuid[:8])
continue
# Получаем предыдущее значение
@@ -474,13 +487,18 @@ class TrafficMonitoringServiceV2:
continue
logger.info(
- f'⚠️ Превышение дельты: {user.uuid[:8]}... +{delta_gb:.2f} ГБ (порог {self.get_fast_check_threshold_gb()} ГБ, previous={previous_bytes / (1024**3):.2f} ГБ, current={current_bytes / (1024**3):.2f} ГБ)'
+ '⚠️ Превышение дельты: ... + ГБ (порог ГБ, previous= ГБ, current= ГБ)',
+ uuid=user.uuid[:8],
+ delta_gb=round(delta_gb, 2),
+ get_fast_check_threshold_gb=self.get_fast_check_threshold_gb(),
+ previous_bytes=round(previous_bytes / 1024**3, 2),
+ current_bytes=round(current_bytes / 1024**3, 2),
)
# Проверяем исключённых пользователей (служебные/тунельные)
if user.uuid.lower() in excluded_user_uuids:
logger.info(
- f'⏭️ Пропускаем {user.uuid[:8]}... - пользователь в списке исключений (служебный/тунельный)'
+ '⏭️ Пропускаем ... пользователь в списке исключений (служебный/тунельный)', uuid=user.uuid[:8]
)
continue
@@ -488,7 +506,9 @@ class TrafficMonitoringServiceV2:
last_node_uuid = user_traffic.last_connected_node_uuid
if not self.should_monitor_node(last_node_uuid):
logger.warning(
- f'⏭️ Пропускаем {user.uuid[:8]} - нода {last_node_uuid or "неизвестна"} не в списке мониторинга'
+ '⏭️ Пропускаем нода не в списке мониторинга',
+ uuid=user.uuid[:8],
+ last_node_uuid=last_node_uuid or 'неизвестна',
)
continue
@@ -509,23 +529,27 @@ class TrafficMonitoringServiceV2:
violations.append(violation)
except Exception as e:
- logger.error(f'❌ Ошибка обработки пользователя {user.uuid}: {e}')
+ logger.error('❌ Ошибка обработки пользователя', uuid=user.uuid, error=e)
# Обновляем snapshot (в Redis с fallback на память)
await self._save_snapshot(new_snapshot)
- logger.info(f'💾 Новый snapshot сохранён: {len(new_snapshot)} пользователей')
+ logger.info('💾 Новый snapshot сохранён: пользователей', new_snapshot_count=len(new_snapshot))
elapsed = (datetime.utcnow() - start_time).total_seconds()
if is_first_run:
logger.info(
- f'✅ Snapshot создан за {elapsed:.1f}с: {len(new_snapshot)} пользователей. '
- f'Следующая проверка покажет превышения.'
+ '✅ Snapshot создан за с: пользователей. Следующая проверка покажет превышения.',
+ elapsed=round(elapsed, 1),
+ new_snapshot_count=len(new_snapshot),
)
else:
logger.info(
- f'✅ Быстрая проверка завершена за {elapsed:.1f}с: '
- f'{len(users)} пользователей, {users_with_delta} с дельтой >0, {len(violations)} превышений'
+ '✅ Быстрая проверка завершена за с: пользователей, с дельтой >0, превышений',
+ elapsed=round(elapsed, 1),
+ users_count=len(users),
+ users_with_delta=users_with_delta,
+ violations_count=len(violations),
)
# Отправляем уведомления только если это не первый запуск
await self._send_violation_notifications(violations, bot)
@@ -604,7 +628,7 @@ class TrafficMonitoringServiceV2:
)
except Exception as e:
- logger.error(f'❌ Ошибка суточной проверки для {user.uuid}: {e}')
+ logger.error('❌ Ошибка суточной проверки для', uuid=user.uuid, error=e)
return None
# Параллельная проверка
@@ -617,8 +641,10 @@ class TrafficMonitoringServiceV2:
elapsed = (datetime.utcnow() - start_time).total_seconds()
logger.info(
- f'✅ Суточная проверка завершена за {elapsed:.1f}с: '
- f'{len(users)} пользователей, {len(violations)} превышений'
+ '✅ Суточная проверка завершена за с: пользователей, превышений',
+ elapsed=round(elapsed, 1),
+ users_count=len(users),
+ violations_count=len(violations),
)
# Отправляем уведомления
@@ -640,7 +666,9 @@ class TrafficMonitoringServiceV2:
max_notifications = 10
if len(violations) > max_notifications:
logger.warning(
- f'⚠️ Слишком много превышений ({len(violations)}), отправляем только первые {max_notifications}'
+ '⚠️ Слишком много превышений отправляем только первые',
+ violations_count=len(violations),
+ max_notifications=max_notifications,
)
violations = violations[:max_notifications]
@@ -648,7 +676,9 @@ class TrafficMonitoringServiceV2:
try:
if not await self.should_send_notification(violation.user_uuid):
logger.info(
- f'⏭️ Кулдаун для {violation.user_uuid[:8]}... - пропускаем уведомление (кулдаун {self.get_notification_cooldown_seconds() // 60} мин)'
+ '⏭️ Кулдаун для ... пропускаем уведомление (кулдаун мин)',
+ user_uuid=violation.user_uuid[:8],
+ value=self.get_notification_cooldown_seconds() // 60,
)
continue
@@ -700,14 +730,14 @@ class TrafficMonitoringServiceV2:
await admin_service.send_suspicious_traffic_notification(message, bot, topic_id)
await self.record_notification(violation.user_uuid)
- logger.info(f'📨 Уведомление отправлено для {violation.user_uuid}')
+ logger.info('📨 Уведомление отправлено для', user_uuid=violation.user_uuid)
# Задержка между отправками (защита от flood)
if i < len(violations) - 1:
await asyncio.sleep(0.5)
except Exception as e:
- logger.error(f'❌ Ошибка отправки уведомления для {violation.user_uuid}: {e}')
+ logger.error('❌ Ошибка отправки уведомления для', user_uuid=violation.user_uuid, error=e)
class TrafficMonitoringSchedulerV2:
@@ -747,14 +777,14 @@ class TrafficMonitoringSchedulerV2:
# Запускаем быструю проверку
if self.service.is_fast_check_enabled():
interval = self.service.get_fast_check_interval_seconds()
- logger.info(f'🚀 Запуск быстрой проверки трафика каждые {interval // 60} мин')
+ logger.info('🚀 Запуск быстрой проверки трафика каждые мин', value=interval // 60)
self._fast_check_task = asyncio.create_task(self._run_fast_check_loop(interval))
# Запускаем суточную проверку
if self.service.is_daily_check_enabled():
check_time = self.service.get_daily_check_time()
if check_time:
- logger.info(f'🚀 Запуск суточной проверки трафика в {check_time.strftime("%H:%M")}')
+ logger.info('🚀 Запуск суточной проверки трафика в', check_time=check_time.strftime('%H:%M'))
self._daily_check_task = asyncio.create_task(self._run_daily_check_loop(check_time))
async def stop(self):
@@ -782,7 +812,7 @@ class TrafficMonitoringSchedulerV2:
async def _run_fast_check_loop(self, interval_seconds: int):
"""Цикл быстрой проверки"""
# Сначала ждём интервал (snapshot уже создан в start())
- logger.info(f'⏳ Первая проверка через {interval_seconds // 60} минут...')
+ logger.info('⏳ Первая проверка через минут...', value=interval_seconds // 60)
await asyncio.sleep(interval_seconds)
while self._is_running:
@@ -793,7 +823,7 @@ class TrafficMonitoringSchedulerV2:
except asyncio.CancelledError:
break
except Exception as e:
- logger.error(f'❌ Ошибка в цикле быстрой проверки: {e}')
+ logger.error('❌ Ошибка в цикле быстрой проверки', error=e)
await asyncio.sleep(interval_seconds)
async def _run_daily_check_loop(self, check_time: time):
@@ -807,7 +837,7 @@ class TrafficMonitoringSchedulerV2:
next_run += timedelta(days=1)
delay = (next_run - now).total_seconds()
- logger.debug(f'⏰ Следующая суточная проверка через {delay / 3600:.1f}ч')
+ logger.debug('⏰ Следующая суточная проверка через ч', delay=round(delay / 3600, 1))
await asyncio.sleep(delay)
@@ -817,7 +847,7 @@ class TrafficMonitoringSchedulerV2:
except asyncio.CancelledError:
break
except Exception as e:
- logger.error(f'❌ Ошибка в цикле суточной проверки: {e}')
+ logger.error('❌ Ошибка в цикле суточной проверки', error=e)
await asyncio.sleep(3600) # Ждём час при ошибке
async def run_fast_check_now(self) -> list[TrafficViolation]:
@@ -878,7 +908,7 @@ class TrafficMonitoringService:
return is_exceeded, traffic_info
except Exception as e:
- logger.error(f'Ошибка проверки трафика для {user_uuid}: {e}')
+ logger.error('Ошибка проверки трафика для', user_uuid=user_uuid, error=e)
return False, {'total_gb': 0, 'nodes': []}
async def process_suspicious_traffic(self, db: AsyncSession, user_uuid: str, traffic_info: dict, bot):
diff --git a/app/services/trial_activation_service.py b/app/services/trial_activation_service.py
index 4fc2c7bb..1b7eaf34 100644
--- a/app/services/trial_activation_service.py
+++ b/app/services/trial_activation_service.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-import logging
from dataclasses import dataclass
+import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -11,7 +11,7 @@ from app.database.crud.user import add_user_balance, subtract_user_balance
from app.database.models import Subscription, TransactionType, User
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
class TrialPaymentError(Exception):
@@ -121,9 +121,9 @@ async def refund_trial_activation_charge(
if not success:
logger.error(
- 'Failed to refund %s kopeks for user %s during trial activation rollback',
- amount_kopeks,
- getattr(user, 'id', '