Refactor metrics handling and remove scheduler

- Removed the metrics scheduler functionality from the bot, transitioning to real-time metrics updates via middleware.
- Enhanced logging for metrics operations across various handlers to improve monitoring and debugging capabilities.
- Integrated metrics tracking for user activities and database errors, providing better insights into bot performance.
- Cleaned up code by removing obsolete comments and unused imports, improving overall readability and maintainability.
This commit is contained in:
2025-09-03 19:18:04 +03:00
parent 650acd5bce
commit ae7bd476bb
14 changed files with 248 additions and 219 deletions

View File

@@ -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,

View File

@@ -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:

View File

@@ -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"),

View File

@@ -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))

View File

@@ -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"""

View File

@@ -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,

View File

@@ -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

View File

@@ -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 = " "

View File

@@ -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:

View File

@@ -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,

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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()