diff --git a/helper_bot/handlers/admin/admin_handlers.py b/helper_bot/handlers/admin/admin_handlers.py index 9f16fbd..c375987 100644 --- a/helper_bot/handlers/admin/admin_handlers.py +++ b/helper_bot/handlers/admin/admin_handlers.py @@ -25,6 +25,13 @@ from helper_bot.handlers.admin.utils import ( ) from logs.custom_logger import logger +# Local imports - metrics +from helper_bot.utils.metrics import ( + metrics, + track_time, + track_errors +) + # Создаем роутер с middleware для проверки доступа admin_router = Router() admin_router.message.middleware(AdminAccessMiddleware()) @@ -38,6 +45,8 @@ admin_router.message.middleware(AdminAccessMiddleware()) ChatTypeFilter(chat_type=["private"]), Command('admin') ) +@track_time("admin_panel", "admin_handlers") +@track_errors("admin_handlers", "admin_panel") async def admin_panel( message: types.Message, state: FSMContext, @@ -62,6 +71,8 @@ async def admin_panel( StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"), F.text == 'Отменить' ) +@track_time("cancel_ban_process", "admin_handlers") +@track_errors("admin_handlers", "cancel_ban_process") async def cancel_ban_process( message: types.Message, state: FSMContext, @@ -81,6 +92,8 @@ async def cancel_ban_process( StateFilter("ADMIN"), F.text == 'Бан (Список)' ) +@track_time("get_last_users", "admin_handlers") +@track_errors("admin_handlers", "get_last_users") async def get_last_users( message: types.Message, state: FSMContext, @@ -112,6 +125,8 @@ async def get_last_users( StateFilter("ADMIN"), F.text == 'Разбан (список)' ) +@track_time("get_banned_users", "admin_handlers") +@track_errors("admin_handlers", "get_banned_users") async def get_banned_users( message: types.Message, state: FSMContext, @@ -141,6 +156,8 @@ async def get_banned_users( StateFilter("ADMIN"), F.text.in_(['Бан по нику', 'Бан по ID']) ) +@track_time("start_ban_process", "admin_handlers") +@track_errors("admin_handlers", "start_ban_process") async def start_ban_process( message: types.Message, state: FSMContext, @@ -162,6 +179,8 @@ async def start_ban_process( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_TARGET") ) +@track_time("process_ban_target", "admin_handlers") +@track_errors("admin_handlers", "process_ban_target") async def process_ban_target( message: types.Message, state: FSMContext, @@ -232,6 +251,8 @@ async def process_ban_target( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DETAILS") ) +@track_time("process_ban_reason", "admin_handlers") +@track_errors("admin_handlers", "process_ban_reason") async def process_ban_reason( message: types.Message, state: FSMContext, @@ -272,6 +293,8 @@ async def process_ban_reason( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DURATION") ) +@track_time("process_ban_duration", "admin_handlers") +@track_errors("admin_handlers", "process_ban_duration") async def process_ban_duration( message: types.Message, state: FSMContext, @@ -315,6 +338,8 @@ async def process_ban_duration( StateFilter("BAN_CONFIRMATION"), F.text == 'Подтвердить' ) +@track_time("confirm_ban", "admin_handlers") +@track_errors("admin_handlers", "confirm_ban") async def confirm_ban( message: types.Message, state: FSMContext, diff --git a/helper_bot/handlers/admin/services.py b/helper_bot/handlers/admin/services.py index cddedc3..1c4289e 100644 --- a/helper_bot/handlers/admin/services.py +++ b/helper_bot/handlers/admin/services.py @@ -5,6 +5,14 @@ from helper_bot.utils.helper_func import add_days_to_date, get_banned_users_butt from helper_bot.handlers.admin.exceptions import UserAlreadyBannedError, InvalidInputError from logs.custom_logger import logger +# Local imports - metrics +from helper_bot.utils.metrics import ( + metrics, + track_time, + track_errors, + db_query_time +) + class User: """Модель пользователя""" @@ -29,6 +37,8 @@ class AdminService: def __init__(self, bot_db): self.bot_db = bot_db + @track_time("get_last_users", "admin_service") + @track_errors("admin_service", "get_last_users") async def get_last_users(self) -> List[User]: """Получить список последних пользователей""" try: @@ -45,6 +55,8 @@ class AdminService: logger.error(f"Ошибка при получении списка последних пользователей: {e}") raise + @track_time("get_banned_users", "admin_service") + @track_errors("admin_service", "get_banned_users") async def get_banned_users(self) -> List[BannedUser]: """Получить список заблокированных пользователей""" try: @@ -68,6 +80,8 @@ class AdminService: logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}") raise + @track_time("get_user_by_username", "admin_service") + @track_errors("admin_service", "get_user_by_username") async def get_user_by_username(self, username: str) -> Optional[User]: """Получить пользователя по username""" try: @@ -85,6 +99,8 @@ class AdminService: logger.error(f"Ошибка при поиске пользователя по username {username}: {e}") raise + @track_time("get_user_by_id", "admin_service") + @track_errors("admin_service", "get_user_by_id") async def get_user_by_id(self, user_id: int) -> Optional[User]: """Получить пользователя по ID""" try: @@ -101,6 +117,8 @@ class AdminService: logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}") raise + @track_time("ban_user", "admin_service") + @track_errors("admin_service", "ban_user") async def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int]) -> None: """Заблокировать пользователя""" try: @@ -122,6 +140,8 @@ class AdminService: logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}") raise + @track_time("unban_user", "admin_service") + @track_errors("admin_service", "unban_user") async def unban_user(self, user_id: int) -> None: """Разблокировать пользователя""" try: @@ -131,6 +151,8 @@ class AdminService: logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}") raise + @track_time("validate_user_input", "admin_service") + @track_errors("admin_service", "validate_user_input") async def validate_user_input(self, input_text: str) -> int: """Валидация введенного ID пользователя""" try: @@ -141,6 +163,8 @@ class AdminService: except ValueError: raise InvalidInputError("ID пользователя должен быть числом") + @track_time("get_banned_users_for_display", "admin_service") + @track_errors("admin_service", "get_banned_users_for_display") async def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]: """Получить данные заблокированных пользователей для отображения""" try: diff --git a/helper_bot/handlers/callback/callback_handlers.py b/helper_bot/handlers/callback/callback_handlers.py index fe3a175..f9e81a8 100644 --- a/helper_bot/handlers/callback/callback_handlers.py +++ b/helper_bot/handlers/callback/callback_handlers.py @@ -24,10 +24,20 @@ from .constants import ( ) from logs.custom_logger import logger +# Local imports - metrics +from helper_bot.utils.metrics import ( + metrics, + track_time, + track_errors, + db_query_time +) + callback_router = Router() @callback_router.callback_query(F.data == CALLBACK_PUBLISH) +@track_time("post_for_group", "callback_handlers") +@track_errors("callback_handlers", "post_for_group") async def post_for_group( call: CallbackQuery, settings: MagicData("settings") @@ -59,6 +69,8 @@ async def post_for_group( @callback_router.callback_query(F.data == CALLBACK_DECLINE) +@track_time("decline_post_for_group", "callback_handlers") +@track_errors("callback_handlers", "decline_post_for_group") async def decline_post_for_group( call: CallbackQuery, settings: MagicData("settings") @@ -89,6 +101,8 @@ async def decline_post_for_group( @callback_router.callback_query(F.data == CALLBACK_BAN) +@track_time("ban_user_from_post", "callback_handlers") +@track_errors("callback_handlers", "ban_user_from_post") async def ban_user_from_post(call: CallbackQuery, **kwargs): ban_service = get_ban_service() # TODO: переделать на MagicData @@ -109,6 +123,8 @@ async def ban_user_from_post(call: CallbackQuery, **kwargs): @callback_router.callback_query(F.data.contains(CALLBACK_BAN)) +@track_time("process_ban_user", "callback_handlers") +@track_errors("callback_handlers", "process_ban_user") async def process_ban_user(call: CallbackQuery, state: FSMContext, **kwargs): ban_service = get_ban_service() # TODO: переделать на MagicData @@ -142,6 +158,8 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext, **kwargs): @callback_router.callback_query(F.data.contains(CALLBACK_UNLOCK)) +@track_time("process_unlock_user", "callback_handlers") +@track_errors("callback_handlers", "process_unlock_user") async def process_unlock_user(call: CallbackQuery, **kwargs): ban_service = get_ban_service() # TODO: переделать на MagicData @@ -166,6 +184,8 @@ async def process_unlock_user(call: CallbackQuery, **kwargs): @callback_router.callback_query(F.data == CALLBACK_RETURN) +@track_time("return_to_main_menu", "callback_handlers") +@track_errors("callback_handlers", "return_to_main_menu") async def return_to_main_menu(call: CallbackQuery, **kwargs): await call.message.delete() logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}") @@ -174,6 +194,8 @@ async def return_to_main_menu(call: CallbackQuery, **kwargs): @callback_router.callback_query(F.data.contains(CALLBACK_PAGE)) +@track_time("change_page", "callback_handlers") +@track_errors("callback_handlers", "change_page") async def change_page( call: CallbackQuery, bot_db: MagicData("bot_db"), @@ -214,6 +236,8 @@ async def change_page( @callback_router.callback_query(F.data == CALLBACK_SAVE) +@track_time("save_voice_message", "callback_handlers") +@track_errors("callback_handlers", "save_voice_message") async def save_voice_message( call: CallbackQuery, bot_db: MagicData("bot_db"), @@ -257,6 +281,8 @@ async def save_voice_message( @callback_router.callback_query(F.data == CALLBACK_DELETE) +@track_time("delete_voice_message", "callback_handlers") +@track_errors("callback_handlers", "delete_voice_message") async def delete_voice_message( call: CallbackQuery, bot_db: MagicData("bot_db"), diff --git a/helper_bot/handlers/callback/services.py b/helper_bot/handlers/callback/services.py index 3606710..bb69930 100644 --- a/helper_bot/handlers/callback/services.py +++ b/helper_bot/handlers/callback/services.py @@ -23,6 +23,14 @@ from .constants import ( ) from logs.custom_logger import logger +# Local imports - metrics +from helper_bot.utils.metrics import ( + metrics, + track_time, + track_errors, + db_query_time +) + class PostPublishService: def __init__(self, bot: Bot, db, settings: Dict[str, Any]): @@ -40,6 +48,8 @@ class PostPublishService: return self.bot return message.bot + @track_time("publish_post", "post_publish_service") + @track_errors("post_publish_service", "publish_post") async def publish_post(self, call: CallbackQuery) -> None: """Основной метод публикации поста""" # Проверяем, является ли сообщение частью медиагруппы @@ -64,6 +74,8 @@ class PostPublishService: else: raise PublishError(f"Неподдерживаемый тип контента: {content_type}") + @track_time("_publish_text_post", "post_publish_service") + @track_errors("post_publish_service", "_publish_text_post") async def _publish_text_post(self, call: CallbackQuery) -> None: """Публикация текстового поста""" text_post = html.escape(str(call.message.text)) @@ -73,6 +85,8 @@ class PostPublishService: await self._delete_post_and_notify_author(call, author_id) logger.info(f'Текст сообщения опубликован в канале {self.main_public}.') + @track_time("_publish_photo_post", "post_publish_service") + @track_errors("post_publish_service", "_publish_photo_post") async def _publish_photo_post(self, call: CallbackQuery) -> None: """Публикация поста с фото""" text_post_with_photo = html.escape(str(call.message.caption)) @@ -82,6 +96,8 @@ class PostPublishService: await self._delete_post_and_notify_author(call, author_id) logger.info(f'Пост с фото опубликован в канале {self.main_public}.') + @track_time("_publish_video_post", "post_publish_service") + @track_errors("post_publish_service", "_publish_video_post") async def _publish_video_post(self, call: CallbackQuery) -> None: """Публикация поста с видео""" text_post_with_photo = html.escape(str(call.message.caption)) @@ -91,6 +107,8 @@ class PostPublishService: await self._delete_post_and_notify_author(call, author_id) logger.info(f'Пост с видео опубликован в канале {self.main_public}.') + @track_time("_publish_video_note_post", "post_publish_service") + @track_errors("post_publish_service", "_publish_video_note_post") async def _publish_video_note_post(self, call: CallbackQuery) -> None: """Публикация поста с кружком""" author_id = await self._get_author_id(call.message.message_id) @@ -99,6 +117,8 @@ class PostPublishService: await self._delete_post_and_notify_author(call, author_id) logger.info(f'Пост с кружком опубликован в канале {self.main_public}.') + @track_time("_publish_audio_post", "post_publish_service") + @track_errors("post_publish_service", "_publish_audio_post") async def _publish_audio_post(self, call: CallbackQuery) -> None: """Публикация поста с аудио""" text_post_with_photo = html.escape(str(call.message.caption)) @@ -108,6 +128,8 @@ class PostPublishService: await self._delete_post_and_notify_author(call, author_id) logger.info(f'Пост с аудио опубликован в канале {self.main_public}.') + @track_time("_publish_voice_post", "post_publish_service") + @track_errors("post_publish_service", "_publish_voice_post") async def _publish_voice_post(self, call: CallbackQuery) -> None: """Публикация поста с войсом""" author_id = await self._get_author_id(call.message.message_id) @@ -116,6 +138,8 @@ class PostPublishService: await self._delete_post_and_notify_author(call, author_id) logger.info(f'Пост с войсом опубликован в канале {self.main_public}.') + @track_time("_publish_media_group", "post_publish_service") + @track_errors("post_publish_service", "_publish_media_group") async def _publish_media_group(self, call: CallbackQuery) -> None: """Публикация медиагруппы""" logger.info(f"Начинаю публикацию медиагруппы. Helper message ID: {call.message.message_id}") @@ -161,6 +185,8 @@ class PostPublishService: logger.error(f"Ошибка при публикации медиагруппы: {e}") raise PublishError(f"Не удалось опубликовать медиагруппу: {str(e)}") + @track_time("decline_post", "post_publish_service") + @track_errors("post_publish_service", "decline_post") async def decline_post(self, call: CallbackQuery) -> None: """Отклонение поста""" logger.info(f"Начинаю отклонение поста. Message ID: {call.message.message_id}, Content type: {call.message.content_type}") @@ -180,6 +206,8 @@ class PostPublishService: logger.error(f"Неподдерживаемый тип контента для отклонения: {content_type}") raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}") + @track_time("_decline_single_post", "post_publish_service") + @track_errors("post_publish_service", "_decline_single_post") async def _decline_single_post(self, call: CallbackQuery) -> None: """Отклонение одиночного поста""" logger.debug(f"Отклоняю одиночный пост. Message ID: {call.message.message_id}") @@ -200,6 +228,8 @@ class PostPublishService: raise logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).') + @track_time("_decline_media_group", "post_publish_service") + @track_errors("post_publish_service", "_decline_media_group") async def _decline_media_group(self, call: CallbackQuery) -> None: """Отклонение медиагруппы""" logger.debug(f"Отклоняю медиагруппу. Helper message ID: {call.message.message_id}") @@ -225,6 +255,8 @@ class PostPublishService: logger.error(f"Ошибка при отправке уведомления автору медиагруппы {author_id}: {e}") raise + @track_time("_get_author_id", "post_publish_service") + @track_errors("post_publish_service", "_get_author_id") async def _get_author_id(self, message_id: int) -> int: """Получение ID автора по ID сообщения""" author_id = await self.db.get_author_id_by_message_id(message_id) @@ -232,6 +264,8 @@ class PostPublishService: raise PostNotFoundError(f"Автор не найден для сообщения {message_id}") return author_id + @track_time("_get_author_id_for_media_group", "post_publish_service") + @track_errors("post_publish_service", "_get_author_id_for_media_group") async def _get_author_id_for_media_group(self, message_id: int) -> int: """Получение ID автора для медиагруппы""" # Сначала пытаемся найти автора по helper_message_id @@ -259,6 +293,8 @@ class PostPublishService: raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}") return author_id + @track_time("_delete_post_and_notify_author", "post_publish_service") + @track_errors("post_publish_service", "_delete_post_and_notify_author") async def _delete_post_and_notify_author(self, call: CallbackQuery, author_id: int) -> None: """Удаление поста и уведомление автора""" await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id) @@ -270,6 +306,8 @@ class PostPublishService: raise UserBlockedBotError("Пользователь заблокировал бота") raise + @track_time("_delete_media_group_and_notify_author", "post_publish_service") + @track_errors("post_publish_service", "_delete_media_group_and_notify_author") async def _delete_media_group_and_notify_author(self, call: CallbackQuery, author_id: int) -> None: """Удаление медиагруппы и уведомление автора""" post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id) @@ -293,6 +331,8 @@ class BanService: self.group_for_posts = settings['Telegram']['group_for_posts'] self.important_logs = settings['Telegram']['important_logs'] + @track_time("ban_user_from_post", "ban_service") + @track_errors("ban_service", "ban_user_from_post") async def ban_user_from_post(self, call: CallbackQuery) -> None: """Бан пользователя за спам""" author_id = await self.db.get_author_id_by_message_id(call.message.message_id) @@ -321,6 +361,8 @@ class BanService: logger.info(f"Пользователь {author_id} заблокирован за спам до {date_str}") + @track_time("ban_user", "ban_service") + @track_errors("ban_service", "ban_user") async def ban_user(self, user_id: str, user_name: str) -> str: """Бан пользователя по ID""" user_name = await self.db.get_username(int(user_id)) @@ -329,6 +371,8 @@ class BanService: return user_name + @track_time("unlock_user", "ban_service") + @track_errors("ban_service", "unlock_user") async def unlock_user(self, user_id: str) -> str: """Разблокировка пользователя""" user_name = await self.db.get_username(int(user_id)) diff --git a/helper_bot/handlers/group/group_handlers.py b/helper_bot/handlers/group/group_handlers.py index 1c5a2ee..4e82227 100644 --- a/helper_bot/handlers/group/group_handlers.py +++ b/helper_bot/handlers/group/group_handlers.py @@ -46,6 +46,8 @@ class GroupHandlers: ) @error_handler + @track_errors("group_handlers", "handle_message") + @track_time("handle_message", "group_handlers") async def handle_message(self, message: types.Message, state: FSMContext, **kwargs): """Handle admin reply to user through group chat""" diff --git a/helper_bot/handlers/group/services.py b/helper_bot/handlers/group/services.py index 9f62d8c..973e1ce 100644 --- a/helper_bot/handlers/group/services.py +++ b/helper_bot/handlers/group/services.py @@ -32,6 +32,8 @@ class AdminReplyService: def __init__(self, db: DatabaseProtocol) -> None: self.db = db + @track_time("get_user_id_for_reply", "admin_reply_service") + @track_errors("admin_reply_service", "get_user_id_for_reply") async def get_user_id_for_reply(self, message_id: int) -> int: """ Get user ID for reply by message ID. @@ -50,6 +52,8 @@ class AdminReplyService: raise UserNotFoundError(f"User not found for message_id: {message_id}") return user_id + @track_time("send_reply_to_user", "admin_reply_service") + @track_errors("admin_reply_service", "send_reply_to_user") async def send_reply_to_user( self, chat_id: int, diff --git a/helper_bot/handlers/private/private_handlers.py b/helper_bot/handlers/private/private_handlers.py index 9730a7d..16db295 100644 --- a/helper_bot/handlers/private/private_handlers.py +++ b/helper_bot/handlers/private/private_handlers.py @@ -29,7 +29,8 @@ from helper_bot.utils.helper_func import ( from helper_bot.utils.metrics import ( metrics, track_time, - track_errors + track_errors, + db_query_time ) # Local imports - modular components @@ -81,6 +82,8 @@ class PrivateHandlers: self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["CHAT"]), ChatTypeFilter(chat_type=["private"])) @error_handler + @track_errors("private_handlers", "handle_emoji_message") + @track_time("handle_emoji_message", "private_handlers") async def handle_emoji_message(self, message: types.Message, state: FSMContext, **kwargs): """Handle emoji command""" await self.user_service.log_user_message(message) @@ -90,6 +93,8 @@ class PrivateHandlers: await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML') @error_handler + @track_errors("private_handlers", "handle_restart_message") + @track_time("handle_restart_message", "private_handlers") async def handle_restart_message(self, message: types.Message, state: FSMContext, **kwargs): """Handle restart command""" markup = await get_reply_keyboard(self.db, message.from_user.id) @@ -100,6 +105,8 @@ class PrivateHandlers: await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML') @error_handler + @track_errors("private_handlers", "handle_start_message") + @track_time("handle_start_message", "private_handlers") async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs): """Handle start command and return to bot button with metrics tracking""" # User service operations with metrics @@ -116,6 +123,8 @@ class PrivateHandlers: await message.answer(hello_message, reply_markup=markup, parse_mode='HTML') @error_handler + @track_errors("private_handlers", "suggest_post") + @track_time("suggest_post", "private_handlers") async def suggest_post(self, message: types.Message, state: FSMContext, **kwargs): """Handle suggest post button""" # User service operations with metrics @@ -128,6 +137,8 @@ class PrivateHandlers: await message.answer(suggest_news, reply_markup=markup) @error_handler + @track_errors("private_handlers", "end_message") + @track_time("end_message", "private_handlers") async def end_message(self, message: types.Message, state: FSMContext, **kwargs): """Handle goodbye button""" # User service operations with metrics @@ -144,6 +155,8 @@ class PrivateHandlers: await state.set_state(FSM_STATES["START"]) @error_handler + @track_errors("private_handlers", "suggest_router") + @track_time("suggest_router", "private_handlers") async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs): """Handle post submission in suggest state""" # Post service operations with metrics @@ -158,6 +171,8 @@ class PrivateHandlers: await state.set_state(FSM_STATES["START"]) @error_handler + @track_errors("private_handlers", "stickers") + @track_time("stickers", "private_handlers") async def stickers(self, message: types.Message, state: FSMContext, **kwargs): """Handle stickers request""" # User service operations with metrics @@ -171,6 +186,8 @@ class PrivateHandlers: await state.set_state(FSM_STATES["START"]) @error_handler + @track_errors("private_handlers", "connect_with_admin") + @track_time("connect_with_admin", "private_handlers") async def connect_with_admin(self, message: types.Message, state: FSMContext, **kwargs): """Handle connect with admin button""" # User service operations with metrics @@ -181,6 +198,8 @@ class PrivateHandlers: await state.set_state(FSM_STATES["PRE_CHAT"]) @error_handler + @track_errors("private_handlers", "resend_message_in_group_for_message") + @track_time("resend_message_in_group_for_message", "private_handlers") async def resend_message_in_group_for_message(self, message: types.Message, state: FSMContext, **kwargs): """Handle messages in admin chat states""" # User service operations with metrics diff --git a/helper_bot/handlers/private/services.py b/helper_bot/handlers/private/services.py index c82ba6f..bc09fb5 100644 --- a/helper_bot/handlers/private/services.py +++ b/helper_bot/handlers/private/services.py @@ -74,7 +74,6 @@ class UserService: @track_time("update_user_activity", "user_service") @track_errors("user_service", "update_user_activity") - @db_query_time("update_user_activity", "user_service") async def update_user_activity(self, user_id: int) -> None: """Update user's last activity timestamp with metrics tracking""" await self.db.update_user_date(user_id) @@ -264,7 +263,7 @@ class PostService: await add_in_db_media(sent_message, self.db) @track_time("handle_media_group_post", "post_service") - @track_errors("post_service", "handle_media_group_post") + @track_errors("post_service", "handle_media_group_post") async def handle_media_group_post(self, message: types.Message, album: list, first_name: str) -> None: """Handle media group post submission""" post_caption = " " diff --git a/helper_bot/handlers/voice/services.py b/helper_bot/handlers/voice/services.py index 44c6578..3c14634 100644 --- a/helper_bot/handlers/voice/services.py +++ b/helper_bot/handlers/voice/services.py @@ -14,6 +14,13 @@ from helper_bot.handlers.voice.constants import ( ) from logs.custom_logger import logger +# Local imports - metrics +from helper_bot.utils.metrics import ( + metrics, + track_time, + track_errors, + db_query_time +) class VoiceMessage: """Модель голосового сообщения""" @@ -30,6 +37,8 @@ class VoiceBotService: self.bot_db = bot_db self.settings = settings + @track_time("get_welcome_sticker", "voice_bot_service") + @track_errors("voice_bot_service", "get_welcome_sticker") async def get_welcome_sticker(self) -> Optional[FSInputFile]: """Получить случайный приветственный стикер""" try: @@ -47,6 +56,8 @@ class VoiceBotService: await self._send_error_to_logs(f'Отправка приветственных стикеров лажает. Ошибка: {e}') return None + @track_time("send_welcome_messages", "voice_bot_service") + @track_errors("voice_bot_service", "send_welcome_messages") async def send_welcome_messages(self, message, user_emoji: str): """Отправить приветственные сообщения""" try: @@ -141,6 +152,8 @@ class VoiceBotService: logger.error(f"Ошибка при отправке приветственных сообщений: {e}") raise VoiceMessageError(f"Не удалось отправить приветственные сообщения: {e}") + @track_time("get_random_audio", "voice_bot_service") + @track_errors("voice_bot_service", "get_random_audio") async def get_random_audio(self, user_id: int) -> Optional[Tuple[str, str, str]]: """Получить случайное аудио для прослушивания""" try: @@ -165,6 +178,8 @@ class VoiceBotService: logger.error(f"Ошибка при получении случайного аудио: {e}") raise AudioProcessingError(f"Не удалось получить случайное аудио: {e}") + @track_time("mark_audio_as_listened", "voice_bot_service") + @track_errors("voice_bot_service", "mark_audio_as_listened") async def mark_audio_as_listened(self, file_name: str, user_id: int) -> None: """Пометить аудио как прослушанное""" try: @@ -173,6 +188,8 @@ class VoiceBotService: logger.error(f"Ошибка при пометке аудио как прослушанного: {e}") raise DatabaseError(f"Не удалось пометить аудио как прослушанное: {e}") + @track_time("clear_user_listenings", "voice_bot_service") + @track_errors("voice_bot_service", "clear_user_listenings") async def clear_user_listenings(self, user_id: int) -> None: """Очистить прослушивания пользователя""" try: @@ -181,6 +198,8 @@ class VoiceBotService: logger.error(f"Ошибка при очистке прослушиваний: {e}") raise DatabaseError(f"Не удалось очистить прослушивания: {e}") + @track_time("get_remaining_audio_count", "voice_bot_service") + @track_errors("voice_bot_service", "get_remaining_audio_count") async def get_remaining_audio_count(self, user_id: int) -> int: """Получить количество оставшихся непрослушанных аудио""" try: @@ -190,11 +209,15 @@ class VoiceBotService: logger.error(f"Ошибка при получении количества аудио: {e}") raise DatabaseError(f"Не удалось получить количество аудио: {e}") + @track_time("get_main_keyboard", "voice_bot_service") + @track_errors("voice_bot_service", "get_main_keyboard") def _get_main_keyboard(self): """Получить основную клавиатуру""" from helper_bot.keyboards.keyboards import get_main_keyboard return get_main_keyboard() + @track_time("send_error_to_logs", "voice_bot_service") + @track_errors("voice_bot_service", "send_error_to_logs") async def _send_error_to_logs(self, message: str) -> None: """Отправить ошибку в логи""" try: @@ -215,6 +238,8 @@ class AudioFileService: def __init__(self, bot_db): self.bot_db = bot_db + @track_time("generate_file_name", "audio_file_service") + @track_errors("audio_file_service", "generate_file_name") async def generate_file_name(self, user_id: int) -> str: """Сгенерировать имя файла для аудио""" try: @@ -245,6 +270,8 @@ class AudioFileService: logger.error(f"Ошибка при генерации имени файла: {e}") raise FileOperationError(f"Не удалось сгенерировать имя файла: {e}") + @track_time("save_audio_file", "audio_file_service") + @track_errors("audio_file_service", "save_audio_file") async def save_audio_file(self, file_name: str, user_id: int, date_added: datetime, file_id: str) -> None: """Сохранить информацию об аудио файле в базу данных""" try: @@ -253,6 +280,8 @@ class AudioFileService: logger.error(f"Ошибка при сохранении аудио файла в БД: {e}") raise DatabaseError(f"Не удалось сохранить аудио файл в БД: {e}") + @track_time("download_and_save_audio", "audio_file_service") + @track_errors("audio_file_service", "download_and_save_audio") async def download_and_save_audio(self, bot, message, file_name: str) -> None: """Скачать и сохранить аудио файл""" try: diff --git a/helper_bot/handlers/voice/voice_handler.py b/helper_bot/handlers/voice/voice_handler.py index ae331ed..71c035e 100644 --- a/helper_bot/handlers/voice/voice_handler.py +++ b/helper_bot/handlers/voice/voice_handler.py @@ -22,6 +22,13 @@ from helper_bot.keyboards import get_reply_keyboard from helper_bot.handlers.private.constants import FSM_STATES from helper_bot.handlers.private.constants import BUTTON_TEXTS +# Local imports - metrics +from helper_bot.utils.metrics import ( + metrics, + track_time, + track_errors, + db_query_time +) class VoiceHandlers: def __init__(self, db, settings): @@ -115,6 +122,8 @@ class VoiceHandlers: F.text == "😊Узнать эмодзи" ) + @track_time("voice_bot_button_handler", "voice_handlers") + @track_errors("voice_handlers", "voice_bot_button_handler") async def voice_bot_button_handler(self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings")): """Обработчик кнопки 'Голосовой бот' из основной клавиатуры""" try: @@ -135,6 +144,8 @@ class VoiceHandlers: # В случае ошибки вызываем start await self.start(message, state, bot_db, settings) + @track_time("restart_function", "voice_handlers") + @track_errors("voice_handlers", "restart_function") async def restart_function( self, message: types.Message, @@ -150,6 +161,8 @@ class VoiceHandlers: await message.answer(text='🎤 Записывайся или слушай!', reply_markup=markup) await state.set_state(STATE_START) + @track_time("handle_emoji_message", "voice_handlers") + @track_errors("voice_handlers", "handle_emoji_message") async def handle_emoji_message( self, message: types.Message, @@ -162,6 +175,8 @@ class VoiceHandlers: if user_emoji is not None: await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML') + @track_time("help_function", "voice_handlers") + @track_errors("voice_handlers", "help_function") async def help_function( self, message: types.Message, @@ -177,6 +192,8 @@ class VoiceHandlers: ) await state.set_state(STATE_START) + @track_time("start", "voice_handlers") + @track_errors("voice_handlers", "start") async def start( self, message: types.Message, @@ -201,6 +218,8 @@ class VoiceHandlers: except Exception as e: logger.error(f"Ошибка при отметке получения приветствия: {e}") + @track_time("cancel_handler", "voice_handlers") + @track_errors("voice_handlers", "cancel_handler") async def cancel_handler( self, message: types.Message, @@ -215,6 +234,8 @@ class VoiceHandlers: await message.answer(text='Добро пожаловать в меню!', reply_markup=markup, parse_mode='HTML') await state.set_state(FSM_STATES["START"]) + @track_time("refresh_listen_function", "voice_handlers") + @track_errors("voice_handlers", "refresh_listen_function") async def refresh_listen_function( self, message: types.Message, @@ -239,6 +260,8 @@ class VoiceHandlers: await state.set_state(STATE_START) + @track_time("standup_write", "voice_handlers") + @track_errors("voice_handlers", "standup_write") async def standup_write( self, message: types.Message, @@ -261,6 +284,8 @@ class VoiceHandlers: await state.set_state(STATE_STANDUP_WRITE) + @track_time("suggest_voice", "voice_handlers") + @track_errors("voice_handlers", "suggest_voice") async def suggest_voice( self, message: types.Message, @@ -299,6 +324,8 @@ class VoiceHandlers: await state.set_state(STATE_STANDUP_WRITE) + @track_time("standup_listen_audio", "voice_handlers") + @track_errors("voice_handlers", "standup_listen_audio") async def standup_listen_audio( self, message: types.Message, diff --git a/helper_bot/main.py b/helper_bot/main.py index 72e4527..0436394 100644 --- a/helper_bot/main.py +++ b/helper_bot/main.py @@ -81,12 +81,8 @@ async def start_bot(bdf): # Запускаем метрики сервер await start_metrics_server(metrics_host, metrics_port) - # Запускаем планировщик метрик для периодического обновления - from .utils.metrics_scheduler import start_metrics_scheduler - await start_metrics_scheduler() - logging.info(f"✅ Метрики сервер запущен на {metrics_host}:{metrics_port}") - logging.info("✅ Планировщик метрик запущен") + logging.info("✅ Метрики будут обновляться в реальном времени через middleware") # Запускаем бота с retry логикой await start_bot_with_retry(bot, dp) diff --git a/helper_bot/middlewares/metrics_middleware.py b/helper_bot/middlewares/metrics_middleware.py index 291d893..574419b 100644 --- a/helper_bot/middlewares/metrics_middleware.py +++ b/helper_bot/middlewares/metrics_middleware.py @@ -162,7 +162,7 @@ class MetricsMiddleware(BaseMiddleware): await bot_db.cursor.execute(total_users_query) total_users_result = await bot_db.cursor.fetchone() total_users = total_users_result[0] if total_users_result else 1 - daily_users_query = "SELECT COUNT(DISTINCT user_id) as active_users FROM our_users WHERE date_changed > datetime('now', '-1 day'))" + daily_users_query = "SELECT COUNT(DISTINCT user_id) as active_users FROM our_users WHERE date_changed > datetime('now', '-1 day')" await bot_db.cursor.execute(daily_users_query) daily_users_result = await bot_db.cursor.fetchone() daily_users = daily_users_result[0] if daily_users_result else 1 @@ -328,8 +328,21 @@ class MetricsMiddleware(BaseMiddleware): # FALLBACK: Record unknown callback if parts: + callback_data = parts[0] + + # Группируем похожие callback'и по паттернам + if callback_data.startswith("ban_") and callback_data[4:].isdigit(): + # callback_ban_123456 -> callback_ban + command = "callback_ban" + elif callback_data.startswith("page_") and callback_data[5:].isdigit(): + # callback_page_2 -> callback_page + command = "callback_page" + else: + # Для остальных неизвестных callback'ов оставляем как есть + command = f"callback_{callback_data[:20]}" + return { - 'command': f"callback_{parts[0][:20]}", + 'command': command, 'user_type': "user" if callback.from_user else "unknown", 'handler_type': "unknown_callback_handler" } diff --git a/helper_bot/utils/metrics.py b/helper_bot/utils/metrics.py index e3f25ee..b0f08e1 100644 --- a/helper_bot/utils/metrics.py +++ b/helper_bot/utils/metrics.py @@ -70,6 +70,14 @@ class BotMetrics: registry=self.registry ) + # Database errors counter + self.db_errors_total = Counter( + 'db_errors_total', + 'Total number of database errors', + ['error_type', 'query_type', 'table_name', 'operation'], + registry=self.registry + ) + # Message processing metrics self.messages_processed_total = Counter( 'messages_processed_total', @@ -92,7 +100,14 @@ class BotMetrics: self.rate_limit_hits_total = Counter( 'rate_limit_hits_total', 'Total number of rate limit hits', - ['limit_type', 'handler_type'], + ['limit_type', 'user_id', 'action'], + registry=self.registry + ) + # User activity metrics + self.user_activity_total = Counter( + 'user_activity_total', + 'Total user activity events', + ['activity_type', 'user_type', 'chat_type'], registry=self.registry ) @@ -121,8 +136,8 @@ class BotMetrics: status=status ).observe(duration) - def set_active_users(self, count: int, user_type: str = "total"): - """Set the number of active users.""" + def set_active_users(self, count: int, user_type: str = "daily"): + """Set the number of active users for a specific type.""" self.active_users.labels(user_type=user_type).set(count) def record_db_query(self, query_type: str, duration: float, table_name: str = "unknown", operation: str = "unknown"): @@ -275,6 +290,12 @@ def db_query_time(query_type: str = "unknown", table_name: str = "unknown", oper except Exception as e: duration = time.time() - start_time metrics.record_db_query(query_type, duration, table_name, operation) + metrics.record_db_error( + type(e).__name__, + query_type, + table_name, + operation + ) metrics.record_error( type(e).__name__, "database", @@ -293,6 +314,12 @@ def db_query_time(query_type: str = "unknown", table_name: str = "unknown", oper except Exception as e: duration = time.time() - start_time metrics.record_db_query(query_type, duration, table_name, operation) + metrics.record_db_error( + type(e).__name__, + query_type, + table_name, + operation + ) metrics.record_error( type(e).__name__, "database", diff --git a/helper_bot/utils/metrics_scheduler.py b/helper_bot/utils/metrics_scheduler.py deleted file mode 100644 index a99fa37..0000000 --- a/helper_bot/utils/metrics_scheduler.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Metrics Scheduler for periodic updates of bot metrics. -Automatically updates active users, system metrics, and other periodic data. -""" - -import asyncio -import logging -from datetime import datetime, timezone, timedelta -from typing import Optional -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from apscheduler.triggers.cron import CronTrigger -from apscheduler.triggers.interval import IntervalTrigger - -from .metrics import metrics -from .base_dependency_factory import get_global_instance - - -class MetricsScheduler: - """Scheduler for periodic metrics updates.""" - - def __init__(self): - self.scheduler = AsyncIOScheduler() - self.logger = logging.getLogger(__name__) - self.bdf = None - self.bot_db = None - self.is_running = False - - async def initialize(self): - """Initialize scheduler with database connection.""" - try: - self.bdf = get_global_instance() - self.bot_db = self.bdf.get_db() - self.logger.info("✅ Metrics scheduler initialized successfully") - except Exception as e: - self.logger.error(f"❌ Failed to initialize metrics scheduler: {e}") - - def start_scheduler(self): - """Start the metrics scheduler.""" - if self.is_running: - self.logger.warning("⚠️ Metrics scheduler is already running") - return - - try: - # Update active users every 5 minutes - self.scheduler.add_job( - self._update_active_users_metric, - IntervalTrigger(minutes=5), - id='update_active_users', - name='Update Active Users Metric', - replace_existing=True - ) - - # Update system metrics every minute - self.scheduler.add_job( - self._update_system_metrics, - IntervalTrigger(minutes=1), - id='update_system_metrics', - name='Update System Metrics', - replace_existing=True - ) - - # Daily metrics reset at midnight - self.scheduler.add_job( - self._daily_metrics_reset, - CronTrigger(hour=0, minute=0), - id='daily_metrics_reset', - name='Daily Metrics Reset', - replace_existing=True - ) - - # Start scheduler - self.scheduler.start() - self.is_running = True - self.logger.info("✅ Metrics scheduler started successfully") - - except Exception as e: - self.logger.error(f"❌ Failed to start metrics scheduler: {e}") - - def stop_scheduler(self): - """Stop the metrics scheduler.""" - if not self.is_running: - self.logger.warning("⚠️ Metrics scheduler is not running") - return - - try: - self.scheduler.shutdown() - self.is_running = False - self.logger.info("✅ Metrics scheduler stopped successfully") - except Exception as e: - self.logger.error(f"❌ Failed to stop metrics scheduler: {e}") - - async def _update_active_users_metric(self): - """Update active users metric from database.""" - try: - if not self.bot_db: - await self.initialize() - if not self.bot_db: - self.logger.warning("⚠️ Cannot update active users: no database connection") - return - - self.logger.debug("📊 Updating active users metric...") - - # Count active users (last 24 hours) - import time - current_timestamp = int(time.time()) - one_day_ago = current_timestamp - (24 * 60 * 60) - - active_users_query = """ - SELECT COUNT(DISTINCT user_id) as active_users - FROM our_users - WHERE date_changed > ? - """ - - await self.bot_db.connect() - await self.bot_db.cursor.execute(active_users_query, (one_day_ago,)) - result = await self.bot_db.cursor.fetchone() - active_users = result[0] if result else 0 - await self.bot_db.close() - - # Update metrics - metrics.set_active_users(active_users, "daily") - metrics.set_active_users(active_users, "total") - - self.logger.debug(f"📊 Active users updated: {active_users}") - - except Exception as e: - self.logger.error(f"❌ Failed to update active users metric: {e}") - # Set fallback value - metrics.set_active_users(0, "daily") - metrics.set_active_users(0, "total") - - async def _update_system_metrics(self): - """Update system-related metrics.""" - try: - # You can add system metrics here (CPU, memory, etc.) - # For now, we'll just log that the job is running - self.logger.debug("📊 System metrics update job running...") - - except Exception as e: - self.logger.error(f"❌ Failed to update system metrics: {e}") - - async def _daily_metrics_reset(self): - """Reset daily metrics at midnight.""" - try: - self.logger.info("🔄 Daily metrics reset job running...") - - # You can add daily metrics reset logic here - # For example, reset daily counters, update retention metrics, etc. - - self.logger.info("✅ Daily metrics reset completed") - - except Exception as e: - self.logger.error(f"❌ Failed to reset daily metrics: {e}") - - async def force_update_active_users(self): - """Force update of active users metric (for testing).""" - await self._update_active_users_metric() - - def get_scheduler_status(self) -> dict: - """Get current scheduler status.""" - if not self.is_running: - return {"status": "stopped", "jobs": 0} - - jobs = self.scheduler.get_jobs() - return { - "status": "running", - "jobs": len(jobs), - "job_details": [ - { - "id": job.id, - "name": job.name, - "next_run": job.next_run_time.isoformat() if job.next_run_time else None - } - for job in jobs - ] - } - - -# Global metrics scheduler instance -metrics_scheduler: Optional[MetricsScheduler] = None - - -def get_metrics_scheduler() -> MetricsScheduler: - """Get global metrics scheduler instance.""" - global metrics_scheduler - if metrics_scheduler is None: - metrics_scheduler = MetricsScheduler() - return metrics_scheduler - - -async def start_metrics_scheduler() -> MetricsScheduler: - """Start metrics scheduler and return instance.""" - global metrics_scheduler - if metrics_scheduler is None: - metrics_scheduler = MetricsScheduler() - - await metrics_scheduler.initialize() - metrics_scheduler.start_scheduler() - return metrics_scheduler - - -def stop_metrics_scheduler(): - """Stop metrics scheduler if running.""" - global metrics_scheduler - if metrics_scheduler: - metrics_scheduler.stop_scheduler()