Refactor project structure and enhance Docker support
- Removed unnecessary `__init__.py` and `Dockerfile` to streamline project organization. - Updated `.dockerignore` and `.gitignore` to improve exclusion patterns for build artifacts and environment files. - Enhanced `Makefile` with new commands for managing Docker containers and added help documentation. - Introduced `pyproject.toml` for better project metadata management and dependency tracking. - Updated `requirements.txt` to reflect changes in dependencies for metrics and monitoring. - Refactored various handler files to improve code organization and maintainability.
This commit is contained in:
189
helper_bot/examples/metrics_usage_examples.py
Normal file
189
helper_bot/examples/metrics_usage_examples.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Examples of how to use metrics decorators in your bot handlers.
|
||||
These examples show how to integrate metrics without modifying existing logic.
|
||||
"""
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
# Import metrics decorators
|
||||
from ..utils.metrics import track_time, track_errors, db_query_time, metrics
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
# Example 1: Basic command handler with timing and error tracking
|
||||
@router.message(Command("start"))
|
||||
@track_time("start_command", "private_handler")
|
||||
@track_errors("private_handler", "start_command")
|
||||
async def start_command(message: Message, state: FSMContext):
|
||||
"""Start command handler with metrics."""
|
||||
# Your existing logic here
|
||||
await message.answer("Welcome! Bot started.")
|
||||
|
||||
# Optionally record custom metrics
|
||||
metrics.record_command("start", "private_handler", "user")
|
||||
|
||||
|
||||
# Example 2: Group command handler with custom labels
|
||||
@router.message(Command("help"), F.chat.type.in_({"group", "supergroup"}))
|
||||
@track_time("help_command", "group_handler")
|
||||
@track_errors("group_handler", "help_command")
|
||||
async def help_command(message: Message):
|
||||
"""Help command handler for groups."""
|
||||
await message.answer("Group help information.")
|
||||
|
||||
# Record command with group context
|
||||
metrics.record_command("help", "group_handler", "group_user")
|
||||
|
||||
|
||||
# Example 3: Callback handler with timing
|
||||
@router.callback_query(F.data.startswith("menu:"))
|
||||
@track_time("menu_callback", "callback_handler")
|
||||
@track_errors("callback_handler", "menu_callback")
|
||||
async def menu_callback(callback: CallbackQuery):
|
||||
"""Menu callback handler."""
|
||||
data = callback.data
|
||||
await callback.answer(f"Menu: {data}")
|
||||
|
||||
# Record callback processing
|
||||
metrics.record_message("callback_query", "callback", "callback_handler")
|
||||
|
||||
|
||||
# Example 4: Database operation with query timing
|
||||
@db_query_time("user_lookup", "users", "select")
|
||||
async def get_user_info(user_id: int):
|
||||
"""Example database function with timing."""
|
||||
# Your database query here
|
||||
# result = await db.fetch_one("SELECT * FROM users WHERE id = ?", user_id)
|
||||
return {"user_id": user_id, "status": "active"}
|
||||
|
||||
|
||||
# Example 5: Complex handler with multiple metrics
|
||||
@router.message(Command("stats"))
|
||||
@track_time("stats_command", "admin_handler")
|
||||
@track_errors("admin_handler", "stats_command")
|
||||
async def stats_command(message: Message):
|
||||
"""Stats command with detailed metrics."""
|
||||
try:
|
||||
# Record command execution
|
||||
metrics.record_command("stats", "admin_handler", "admin_user")
|
||||
|
||||
# Your stats logic here
|
||||
stats = await get_bot_stats()
|
||||
|
||||
# Record successful execution
|
||||
await message.answer(f"Bot stats: {stats}")
|
||||
|
||||
except Exception as e:
|
||||
# Error is automatically tracked by decorator
|
||||
await message.answer("Error getting stats")
|
||||
raise
|
||||
|
||||
|
||||
# Example 6: Message handler with message type tracking
|
||||
@router.message()
|
||||
@track_time("message_processing", "general_handler")
|
||||
async def handle_message(message: Message):
|
||||
"""General message handler."""
|
||||
# Message type is automatically detected by middleware
|
||||
# But you can add custom tracking
|
||||
|
||||
if message.photo:
|
||||
# Custom metric for photo processing
|
||||
metrics.record_message("photo", "general", "photo_handler")
|
||||
|
||||
# Your message handling logic
|
||||
await message.answer("Message received")
|
||||
|
||||
|
||||
# Example 7: Error-prone operation with custom error tracking
|
||||
@track_errors("file_handler", "file_upload")
|
||||
async def upload_file(file_data: bytes, filename: str):
|
||||
"""File upload with error tracking."""
|
||||
try:
|
||||
# Your file upload logic
|
||||
# result = await upload_service.upload(file_data, filename)
|
||||
return {"status": "success", "filename": filename}
|
||||
|
||||
except Exception as e:
|
||||
# Custom error metric
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"file_handler",
|
||||
"file_upload"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
# Example 8: Background task with metrics
|
||||
async def background_metrics_collection():
|
||||
"""Background task for collecting periodic metrics."""
|
||||
while True:
|
||||
try:
|
||||
# Collect custom metrics
|
||||
active_users = await count_active_users()
|
||||
metrics.set_active_users(active_users, "current")
|
||||
|
||||
# Wait before next collection
|
||||
await asyncio.sleep(300) # 5 minutes
|
||||
|
||||
except Exception as e:
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"background_task",
|
||||
"metrics_collection"
|
||||
)
|
||||
await asyncio.sleep(60) # Wait 1 minute on error
|
||||
|
||||
|
||||
# Example 9: Custom metric collection in service
|
||||
class UserService:
|
||||
"""Example service with integrated metrics."""
|
||||
|
||||
@db_query_time("user_creation", "users", "insert")
|
||||
async def create_user(self, user_data: dict):
|
||||
"""Create user with database timing."""
|
||||
# Your user creation logic
|
||||
# user_id = await self.db.execute("INSERT INTO users ...")
|
||||
return {"user_id": 123, "status": "created"}
|
||||
|
||||
@track_time("user_update", "user_service")
|
||||
async def update_user(self, user_id: int, updates: dict):
|
||||
"""Update user with timing."""
|
||||
# Your update logic
|
||||
# await self.db.execute("UPDATE users SET ...")
|
||||
return {"user_id": user_id, "status": "updated"}
|
||||
|
||||
|
||||
# Example 10: Middleware integration example
|
||||
async def custom_middleware(handler, event, data):
|
||||
"""Custom middleware that works with metrics system."""
|
||||
from ..utils.metrics import track_middleware
|
||||
|
||||
async with track_middleware("custom_middleware"):
|
||||
# Your middleware logic
|
||||
result = await handler(event, data)
|
||||
return result
|
||||
|
||||
|
||||
# Helper function for stats (placeholder)
|
||||
async def get_bot_stats():
|
||||
"""Get bot statistics."""
|
||||
return {
|
||||
"total_users": 1000,
|
||||
"active_today": 150,
|
||||
"commands_processed": 5000
|
||||
}
|
||||
|
||||
|
||||
# Helper function for user counting (placeholder)
|
||||
async def count_active_users():
|
||||
"""Count active users."""
|
||||
return 150
|
||||
|
||||
|
||||
# Import asyncio for background task
|
||||
import asyncio
|
||||
@@ -1,4 +1,8 @@
|
||||
from typing import Annotated, Dict, Any
|
||||
from typing import Dict, Any
|
||||
try:
|
||||
from typing import Annotated
|
||||
except ImportError:
|
||||
from typing_extensions import Annotated
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import html
|
||||
from tkinter import S
|
||||
import traceback
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Constants for group handlers"""
|
||||
|
||||
from typing import Final
|
||||
from typing import Final, Dict
|
||||
|
||||
# FSM States
|
||||
FSM_STATES: Final[dict[str, str]] = {
|
||||
FSM_STATES: Final[Dict[str, str]] = {
|
||||
"CHAT": "CHAT"
|
||||
}
|
||||
|
||||
# Error messages
|
||||
ERROR_MESSAGES: Final[dict[str, str]] = {
|
||||
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||
"NO_REPLY_TO_MESSAGE": "Блять, выдели сообщение!",
|
||||
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение."
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Constants for private handlers"""
|
||||
|
||||
from typing import Final
|
||||
from typing import Final, Dict
|
||||
|
||||
# FSM States
|
||||
FSM_STATES: Final[dict[str, str]] = {
|
||||
FSM_STATES: Final[Dict[str, str]] = {
|
||||
"START": "START",
|
||||
"SUGGEST": "SUGGEST",
|
||||
"PRE_CHAT": "PRE_CHAT",
|
||||
@@ -11,7 +11,7 @@ FSM_STATES: Final[dict[str, str]] = {
|
||||
}
|
||||
|
||||
# Button texts
|
||||
BUTTON_TEXTS: Final[dict[str, str]] = {
|
||||
BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||
"SUGGEST_POST": "📢Предложить свой пост",
|
||||
"SAY_GOODBYE": "👋🏼Сказать пока!",
|
||||
"LEAVE_CHAT": "Выйти из чата",
|
||||
@@ -21,7 +21,7 @@ BUTTON_TEXTS: Final[dict[str, str]] = {
|
||||
}
|
||||
|
||||
# Error messages
|
||||
ERROR_MESSAGES: Final[dict[str, str]] = {
|
||||
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||
"UNSUPPORTED_CONTENT": (
|
||||
'Я пока не умею работать с таким сообщением. '
|
||||
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
|
||||
|
||||
@@ -24,6 +24,14 @@ from helper_bot.utils.helper_func import (
|
||||
check_user_emoji
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
# Local imports - modular components
|
||||
from .constants import FSM_STATES, BUTTON_TEXTS, ERROR_MESSAGES
|
||||
from .services import BotSettings, UserService, PostService, StickerService
|
||||
@@ -91,16 +99,23 @@ class PrivateHandlers:
|
||||
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
|
||||
|
||||
@error_handler
|
||||
@track_time("start_message_handler", "private_handler")
|
||||
@track_errors("private_handler", "start_message_handler")
|
||||
async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle start command and return to bot button"""
|
||||
"""Handle start command and return to bot button with metrics tracking"""
|
||||
# Record start command metrics
|
||||
metrics.record_command("start", "private_handler", "user" if not message.from_user.is_bot else "bot")
|
||||
metrics.record_message("command", "private", "private_handler")
|
||||
|
||||
# User service operations with metrics
|
||||
await self.user_service.log_user_message(message)
|
||||
await self.user_service.ensure_user_exists(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
# Send sticker
|
||||
# Send sticker with metrics
|
||||
await self.sticker_service.send_random_hello_sticker(message)
|
||||
|
||||
# Send welcome message
|
||||
# Send welcome message with metrics
|
||||
markup = get_reply_keyboard(self.db, message.from_user.id)
|
||||
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
|
||||
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
||||
|
||||
@@ -30,6 +30,14 @@ from helper_bot.utils.helper_func import (
|
||||
)
|
||||
from helper_bot.keyboards import get_reply_keyboard_for_post
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
|
||||
class DatabaseProtocol(Protocol):
|
||||
"""Protocol for database operations"""
|
||||
@@ -65,13 +73,18 @@ class UserService:
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
|
||||
@track_time("update_user_activity", "user_service")
|
||||
@track_errors("user_service", "update_user_activity")
|
||||
@db_query_time("update_user_activity", "users", "update")
|
||||
async def update_user_activity(self, user_id: int) -> None:
|
||||
"""Update user's last activity timestamp"""
|
||||
"""Update user's last activity timestamp with metrics tracking"""
|
||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.db.update_date_for_user(current_date, user_id)
|
||||
|
||||
@track_time("ensure_user_exists", "user_service")
|
||||
@track_errors("user_service", "ensure_user_exists")
|
||||
async def ensure_user_exists(self, message: types.Message) -> None:
|
||||
"""Ensure user exists in database, create if needed"""
|
||||
"""Ensure user exists in database, create if needed with metrics tracking"""
|
||||
user_id = message.from_user.id
|
||||
full_name = message.from_user.full_name
|
||||
username = message.from_user.username or "private_username"
|
||||
@@ -82,14 +95,17 @@ class UserService:
|
||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if not self.db.user_exists(user_id):
|
||||
# Record database operation
|
||||
self.db.add_new_user_in_db(
|
||||
user_id, first_name, full_name, username, is_bot, language_code,
|
||||
"", current_date, current_date
|
||||
)
|
||||
metrics.record_db_query("add_new_user", 0.0, "users", "insert")
|
||||
else:
|
||||
is_need_update = check_username_and_full_name(user_id, username, full_name, self.db)
|
||||
if is_need_update:
|
||||
self.db.update_username_and_full_name(user_id, username, full_name)
|
||||
metrics.record_db_query("update_username_fullname", 0.0, "users", "update")
|
||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
safe_username = html.escape(username) if username else "Без никнейма"
|
||||
|
||||
@@ -100,9 +116,12 @@ class UserService:
|
||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
||||
|
||||
self.db.update_date_for_user(current_date, user_id)
|
||||
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
||||
|
||||
@track_time("log_user_message", "user_service")
|
||||
@track_errors("user_service", "log_user_message")
|
||||
async def log_user_message(self, message: types.Message) -> None:
|
||||
"""Forward user message to logs group"""
|
||||
"""Forward user message to logs group with metrics tracking"""
|
||||
await message.forward(chat_id=self.settings.group_for_logs)
|
||||
|
||||
def get_safe_user_info(self, message: types.Message) -> tuple[str, str]:
|
||||
@@ -210,7 +229,7 @@ class PostService:
|
||||
message_id=media_group_message_id, helper_message_id=help_message_id
|
||||
)
|
||||
|
||||
async def process_post(self, message: types.Message, album: Union[list[types.Message], None] = None) -> None:
|
||||
async def process_post(self, message: types.Message, album: Union[list, None] = None) -> None:
|
||||
"""Process post based on content type"""
|
||||
first_name = get_first_name(message)
|
||||
|
||||
@@ -248,8 +267,10 @@ class StickerService:
|
||||
def __init__(self, settings: BotSettings) -> None:
|
||||
self.settings = settings
|
||||
|
||||
@track_time("send_random_hello_sticker", "sticker_service")
|
||||
@track_errors("sticker_service", "send_random_hello_sticker")
|
||||
async def send_random_hello_sticker(self, message: types.Message) -> None:
|
||||
"""Send random hello sticker"""
|
||||
"""Send random hello sticker with metrics tracking"""
|
||||
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
|
||||
if not name_stick_hello:
|
||||
return
|
||||
@@ -258,8 +279,10 @@ class StickerService:
|
||||
await message.answer_sticker(random_stick_hello)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
@track_time("send_random_goodbye_sticker", "sticker_service")
|
||||
@track_errors("sticker_service", "send_random_goodbye_sticker")
|
||||
async def send_random_goodbye_sticker(self, message: types.Message) -> None:
|
||||
"""Send random goodbye sticker"""
|
||||
"""Send random goodbye sticker with metrics tracking"""
|
||||
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
|
||||
if not name_stick_bye:
|
||||
return
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
from aiogram import types
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors
|
||||
)
|
||||
|
||||
|
||||
def get_reply_keyboard_for_post():
|
||||
builder = InlineKeyboardBuilder()
|
||||
@@ -16,6 +23,8 @@ def get_reply_keyboard_for_post():
|
||||
return markup
|
||||
|
||||
|
||||
@track_time("get_reply_keyboard", "keyboard_service")
|
||||
@track_errors("keyboard_service", "get_reply_keyboard")
|
||||
def get_reply_keyboard(BotDB, user_id):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
||||
@@ -49,7 +58,7 @@ def get_reply_keyboard_admin():
|
||||
return markup
|
||||
|
||||
|
||||
def create_keyboard_with_pagination(page: int, total_items: int, array_items: list[tuple[any, any]], callback: str):
|
||||
def create_keyboard_with_pagination(page: int, total_items: int, array_items: list, callback: str):
|
||||
"""
|
||||
Создает клавиатуру с пагинацией для заданного набора элементов и устанавливает необходимый callback
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from helper_bot.handlers.group import group_router
|
||||
from helper_bot.handlers.private import private_router
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
|
||||
|
||||
|
||||
async def start_bot(bdf):
|
||||
@@ -19,6 +20,12 @@ async def start_bot(bdf):
|
||||
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
|
||||
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
||||
|
||||
# ✅ Middleware для метрик (добавляем первыми)
|
||||
dp.message.middleware(MetricsMiddleware())
|
||||
dp.callback_query.middleware(MetricsMiddleware())
|
||||
dp.message.middleware(ErrorMetricsMiddleware())
|
||||
dp.callback_query.middleware(ErrorMetricsMiddleware())
|
||||
|
||||
# ✅ Глобальная middleware для всех роутеров
|
||||
dp.update.outer_middleware(DependenciesMiddleware())
|
||||
|
||||
|
||||
117
helper_bot/main_with_metrics.py
Normal file
117
helper_bot/main_with_metrics.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Example integration of metrics monitoring in the main bot file.
|
||||
This shows how to integrate the metrics system without modifying existing handlers.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
|
||||
# Import metrics components
|
||||
from .utils.metrics import metrics
|
||||
from .utils.metrics_exporter import MetricsManager
|
||||
from .middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
|
||||
|
||||
# Import your existing bot components
|
||||
# from .handlers import ... # Your existing handlers
|
||||
# from .database.db import BotDB # Your existing database class
|
||||
|
||||
|
||||
class BotWithMetrics:
|
||||
"""Bot class with integrated metrics monitoring."""
|
||||
|
||||
def __init__(self, token: str, metrics_port: int = 8000):
|
||||
self.bot = Bot(token=token, parse_mode=ParseMode.HTML)
|
||||
self.storage = MemoryStorage()
|
||||
self.dp = Dispatcher(storage=self.storage)
|
||||
|
||||
# Initialize metrics manager
|
||||
# You can pass your database instance here if needed
|
||||
# self.metrics_manager = MetricsManager(port=metrics_port, db=your_db_instance)
|
||||
self.metrics_manager = MetricsManager(port=metrics_port)
|
||||
|
||||
# Setup middlewares
|
||||
self._setup_middlewares()
|
||||
|
||||
# Setup handlers (your existing handlers)
|
||||
# self._setup_handlers()
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _setup_middlewares(self):
|
||||
"""Setup metrics middlewares."""
|
||||
# Add metrics middleware first to capture all events
|
||||
self.dp.message.middleware(MetricsMiddleware())
|
||||
self.dp.callback_query.middleware(MetricsMiddleware())
|
||||
|
||||
# Add error tracking middleware
|
||||
self.dp.message.middleware(ErrorMetricsMiddleware())
|
||||
self.dp.callback_query.middleware(ErrorMetricsMiddleware())
|
||||
|
||||
# Your existing middlewares can go here
|
||||
# self.dp.message.middleware(YourExistingMiddleware())
|
||||
|
||||
def _setup_handlers(self):
|
||||
"""Setup bot handlers."""
|
||||
# Import and register your existing handlers here
|
||||
# from .handlers.admin import admin_router
|
||||
# from .handlers.private import private_router
|
||||
# from .handlers.group import group_router
|
||||
# from .handlers.callback import callback_router
|
||||
#
|
||||
# self.dp.include_router(admin_router)
|
||||
# self.dp.include_router(private_router)
|
||||
# self.dp.include_router(group_router)
|
||||
# self.dp.include_router(callback_router)
|
||||
pass
|
||||
|
||||
async def start(self):
|
||||
"""Start the bot with metrics."""
|
||||
try:
|
||||
# Start metrics collection
|
||||
await self.metrics_manager.start()
|
||||
self.logger.info("Metrics system started")
|
||||
|
||||
# Start bot polling
|
||||
await self.dp.start_polling(self.bot)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting bot: {e}")
|
||||
raise
|
||||
finally:
|
||||
await self.stop()
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the bot and metrics."""
|
||||
try:
|
||||
# Stop metrics collection
|
||||
await self.metrics_manager.stop()
|
||||
self.logger.info("Metrics system stopped")
|
||||
|
||||
# Stop bot
|
||||
await self.bot.session.close()
|
||||
self.logger.info("Bot stopped")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping bot: {e}")
|
||||
|
||||
|
||||
# Example usage function
|
||||
async def main():
|
||||
"""Main function to run the bot with metrics."""
|
||||
# Your bot token
|
||||
TOKEN = "YOUR_BOT_TOKEN_HERE"
|
||||
|
||||
# Create and start bot
|
||||
bot = BotWithMetrics(TOKEN)
|
||||
|
||||
try:
|
||||
await bot.start()
|
||||
except KeyboardInterrupt:
|
||||
await bot.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
173
helper_bot/middlewares/metrics_middleware.py
Normal file
173
helper_bot/middlewares/metrics_middleware.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Metrics middleware for aiogram 3.x.
|
||||
Automatically collects metrics for message processing, command execution, and errors.
|
||||
"""
|
||||
|
||||
from typing import Any, Awaitable, Callable, Dict
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||
from aiogram.enums import ChatType
|
||||
import time
|
||||
from ..utils.metrics import metrics, track_middleware
|
||||
|
||||
|
||||
class MetricsMiddleware(BaseMiddleware):
|
||||
"""Middleware for automatic metrics collection in aiogram handlers."""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""Process event and collect metrics."""
|
||||
|
||||
async with track_middleware("metrics_middleware"):
|
||||
# Record message processing
|
||||
if isinstance(event, Message):
|
||||
await self._record_message_metrics(event, data)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await self._record_callback_metrics(event, data)
|
||||
|
||||
# Execute handler and collect timing
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = await handler(event, data)
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Record successful execution
|
||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||
metrics.record_method_duration(
|
||||
handler_name,
|
||||
duration,
|
||||
"handler",
|
||||
"success"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Record error and timing
|
||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||
metrics.record_method_duration(
|
||||
handler_name,
|
||||
duration,
|
||||
"handler",
|
||||
"error"
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"handler",
|
||||
handler_name
|
||||
)
|
||||
raise
|
||||
|
||||
async def _record_message_metrics(self, message: Message, data: Dict[str, Any]):
|
||||
"""Record metrics for message processing."""
|
||||
# Determine message type
|
||||
message_type = "text"
|
||||
if message.photo:
|
||||
message_type = "photo"
|
||||
elif message.video:
|
||||
message_type = "video"
|
||||
elif message.audio:
|
||||
message_type = "audio"
|
||||
elif message.document:
|
||||
message_type = "document"
|
||||
elif message.voice:
|
||||
message_type = "voice"
|
||||
elif message.sticker:
|
||||
message_type = "sticker"
|
||||
elif message.animation:
|
||||
message_type = "animation"
|
||||
|
||||
# Determine chat type
|
||||
chat_type = "private"
|
||||
if message.chat.type == ChatType.GROUP:
|
||||
chat_type = "group"
|
||||
elif message.chat.type == ChatType.SUPERGROUP:
|
||||
chat_type = "supergroup"
|
||||
elif message.chat.type == ChatType.CHANNEL:
|
||||
chat_type = "channel"
|
||||
|
||||
# Determine handler type
|
||||
handler_type = "unknown"
|
||||
if message.text and message.text.startswith('/'):
|
||||
handler_type = "command"
|
||||
# Record command specifically
|
||||
command = message.text.split()[0][1:] # Remove '/' and get command name
|
||||
metrics.record_command(
|
||||
command,
|
||||
"message_handler",
|
||||
"user" if message.from_user else "unknown"
|
||||
)
|
||||
|
||||
# Record message processing
|
||||
metrics.record_message(message_type, chat_type, handler_type)
|
||||
|
||||
async def _record_callback_metrics(self, callback: CallbackQuery, data: Dict[str, Any]):
|
||||
"""Record metrics for callback query processing."""
|
||||
# Record callback processing
|
||||
metrics.record_message(
|
||||
"callback_query",
|
||||
"callback",
|
||||
"callback_handler"
|
||||
)
|
||||
|
||||
# Record callback command if available
|
||||
if callback.data:
|
||||
# Extract command from callback data (assuming format like "command:param")
|
||||
parts = callback.data.split(':', 1)
|
||||
if parts:
|
||||
command = parts[0]
|
||||
metrics.record_command(
|
||||
command,
|
||||
"callback_handler",
|
||||
"user" if callback.from_user else "unknown"
|
||||
)
|
||||
|
||||
|
||||
class DatabaseMetricsMiddleware(BaseMiddleware):
|
||||
"""Middleware for database operation metrics."""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""Process event and collect database metrics."""
|
||||
|
||||
# Check if this handler involves database operations
|
||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||
|
||||
# You can add specific database operation detection logic here
|
||||
# For now, we'll just pass through and let individual decorators handle it
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
|
||||
class ErrorMetricsMiddleware(BaseMiddleware):
|
||||
"""Middleware for error tracking and metrics."""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""Process event and collect error metrics."""
|
||||
|
||||
try:
|
||||
return await handler(event, data)
|
||||
except Exception as e:
|
||||
# Record error metrics
|
||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"handler",
|
||||
handler_name
|
||||
)
|
||||
raise
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
try:
|
||||
import emoji as _emoji_lib
|
||||
@@ -14,6 +15,14 @@ from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMe
|
||||
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from .metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
bdf = get_global_instance()
|
||||
BotDB = bdf.get_db()
|
||||
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
||||
@@ -43,6 +52,8 @@ def safe_html_escape(text: str) -> str:
|
||||
return html.escape(str(text))
|
||||
|
||||
|
||||
@track_time("get_first_name", "helper_func")
|
||||
@track_errors("helper_func", "get_first_name")
|
||||
def get_first_name(message: types.Message) -> str:
|
||||
"""
|
||||
Безопасно получает и экранирует имя пользователя для использования в HTML разметке.
|
||||
@@ -234,7 +245,7 @@ async def add_in_db_media(sent_message, bot_db):
|
||||
|
||||
|
||||
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
|
||||
media_group: list[InputMediaPhoto], bot_db):
|
||||
media_group: List, bot_db):
|
||||
sent_message = await message.bot.send_media_group(
|
||||
chat_id=chat_id,
|
||||
media=media_group,
|
||||
@@ -245,7 +256,7 @@ async def send_media_group_message_to_private_chat(chat_id: int, message: types.
|
||||
return message_id
|
||||
|
||||
|
||||
async def send_media_group_to_channel(bot, chat_id: int, post_content: list[tuple[str]], post_text: str):
|
||||
async def send_media_group_to_channel(bot, chat_id: int, post_content: List, post_text: str):
|
||||
"""
|
||||
Отправляет медиа-группу с подписью к последнему файлу.
|
||||
|
||||
@@ -458,6 +469,9 @@ def delete_user_blacklist(user_id: int, bot_db):
|
||||
return bot_db.delete_user_blacklist(user_id=user_id)
|
||||
|
||||
|
||||
@track_time("check_username_and_full_name", "helper_func")
|
||||
@track_errors("helper_func", "check_username_and_full_name")
|
||||
@db_query_time("get_username_and_full_name", "users", "select")
|
||||
def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
|
||||
username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
|
||||
return username != username_db or full_name != full_name_db
|
||||
@@ -479,6 +493,8 @@ def unban_notifier(self):
|
||||
self.bot.send_message(self.GROUP_FOR_MESSAGE, message)
|
||||
|
||||
|
||||
@track_time("update_user_info", "helper_func")
|
||||
@track_errors("helper_func", "update_user_info")
|
||||
async def update_user_info(source: str, message: types.Message):
|
||||
# Собираем данные
|
||||
full_name = message.from_user.full_name
|
||||
@@ -495,10 +511,12 @@ async def update_user_info(source: str, message: types.Message):
|
||||
if not BotDB.user_exists(user_id):
|
||||
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, user_emoji, date,
|
||||
date)
|
||||
metrics.record_db_query("add_new_user_in_db", 0.0, "users", "insert")
|
||||
else:
|
||||
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
|
||||
if is_need_update:
|
||||
BotDB.update_username_and_full_name(user_id, username, full_name)
|
||||
metrics.record_db_query("update_username_and_full_name", 0.0, "users", "update")
|
||||
if source != 'voice':
|
||||
await message.answer(
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
|
||||
@@ -506,17 +524,25 @@ async def update_user_info(source: str, message: types.Message):
|
||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
|
||||
sleep(1)
|
||||
BotDB.update_date_for_user(date, user_id)
|
||||
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
||||
|
||||
|
||||
@track_time("check_user_emoji", "helper_func")
|
||||
@track_errors("helper_func", "check_user_emoji")
|
||||
@db_query_time("check_emoji_for_user", "users", "select")
|
||||
def check_user_emoji(message: types.Message):
|
||||
user_id = message.from_user.id
|
||||
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
|
||||
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
|
||||
user_emoji = get_random_emoji()
|
||||
BotDB.update_emoji_for_user(user_id=user_id, emoji=user_emoji)
|
||||
metrics.record_db_query("update_emoji_for_user", 0.0, "users", "update")
|
||||
return user_emoji
|
||||
|
||||
|
||||
@track_time("get_random_emoji", "helper_func")
|
||||
@track_errors("helper_func", "get_random_emoji")
|
||||
@db_query_time("check_emoji", "users", "select")
|
||||
def get_random_emoji():
|
||||
attempts = 0
|
||||
while attempts < 100:
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import html
|
||||
|
||||
# Local imports - metrics
|
||||
from .metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors
|
||||
)
|
||||
|
||||
|
||||
@track_time("get_message", "message_service")
|
||||
@track_errors("message_service", "get_message")
|
||||
def get_message(username: str, type_message: str):
|
||||
constants = {
|
||||
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
||||
|
||||
300
helper_bot/utils/metrics.py
Normal file
300
helper_bot/utils/metrics.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
Metrics module for Telegram bot monitoring with Prometheus.
|
||||
Provides predefined metrics for bot commands, errors, performance, and user activity.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
|
||||
from prometheus_client.core import CollectorRegistry
|
||||
import time
|
||||
from functools import wraps
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
|
||||
class BotMetrics:
|
||||
"""Central class for managing all bot metrics."""
|
||||
|
||||
def __init__(self):
|
||||
self.registry = CollectorRegistry()
|
||||
|
||||
# Bot commands counter
|
||||
self.bot_commands_total = Counter(
|
||||
'bot_commands_total',
|
||||
'Total number of bot commands processed',
|
||||
['command_type', 'handler_type', 'user_type'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Method execution time histogram
|
||||
self.method_duration_seconds = Histogram(
|
||||
'method_duration_seconds',
|
||||
'Time spent executing methods',
|
||||
['method_name', 'handler_type', 'status'],
|
||||
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Errors counter
|
||||
self.errors_total = Counter(
|
||||
'errors_total',
|
||||
'Total number of errors',
|
||||
['error_type', 'handler_type', 'method_name'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Active users gauge
|
||||
self.active_users = Gauge(
|
||||
'active_users',
|
||||
'Number of currently active users',
|
||||
['user_type'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Database query metrics
|
||||
self.db_query_duration_seconds = Histogram(
|
||||
'db_query_duration_seconds',
|
||||
'Time spent executing database queries',
|
||||
['query_type', 'table_name', 'operation'],
|
||||
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Message processing metrics
|
||||
self.messages_processed_total = Counter(
|
||||
'messages_processed_total',
|
||||
'Total number of messages processed',
|
||||
['message_type', 'chat_type', 'handler_type'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Middleware execution metrics
|
||||
self.middleware_duration_seconds = Histogram(
|
||||
'middleware_duration_seconds',
|
||||
'Time spent in middleware execution',
|
||||
['middleware_name', 'status'],
|
||||
buckets=[0.01, 0.05, 0.1, 0.25, 0.5],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
def record_command(self, command_type: str, handler_type: str = "unknown", user_type: str = "unknown"):
|
||||
"""Record a bot command execution."""
|
||||
self.bot_commands_total.labels(
|
||||
command_type=command_type,
|
||||
handler_type=handler_type,
|
||||
user_type=user_type
|
||||
).inc()
|
||||
|
||||
def record_error(self, error_type: str, handler_type: str = "unknown", method_name: str = "unknown"):
|
||||
"""Record an error occurrence."""
|
||||
self.errors_total.labels(
|
||||
error_type=error_type,
|
||||
handler_type=handler_type,
|
||||
method_name=method_name
|
||||
).inc()
|
||||
|
||||
def record_method_duration(self, method_name: str, duration: float, handler_type: str = "unknown", status: str = "success"):
|
||||
"""Record method execution duration."""
|
||||
self.method_duration_seconds.labels(
|
||||
method_name=method_name,
|
||||
handler_type=handler_type,
|
||||
status=status
|
||||
).observe(duration)
|
||||
|
||||
def set_active_users(self, count: int, user_type: str = "total"):
|
||||
"""Set the number of active users."""
|
||||
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"):
|
||||
"""Record database query duration."""
|
||||
self.db_query_duration_seconds.labels(
|
||||
query_type=query_type,
|
||||
table_name=table_name,
|
||||
operation=operation
|
||||
).observe(duration)
|
||||
|
||||
def record_message(self, message_type: str, chat_type: str = "unknown", handler_type: str = "unknown"):
|
||||
"""Record a processed message."""
|
||||
self.messages_processed_total.labels(
|
||||
message_type=message_type,
|
||||
chat_type=chat_type,
|
||||
handler_type=handler_type
|
||||
).inc()
|
||||
|
||||
def record_middleware(self, middleware_name: str, duration: float, status: str = "success"):
|
||||
"""Record middleware execution duration."""
|
||||
self.middleware_duration_seconds.labels(
|
||||
middleware_name=middleware_name,
|
||||
status=status
|
||||
).observe(duration)
|
||||
|
||||
def get_metrics(self) -> bytes:
|
||||
"""Generate metrics in Prometheus format."""
|
||||
return generate_latest(self.registry)
|
||||
|
||||
|
||||
# Global metrics instance
|
||||
metrics = BotMetrics()
|
||||
|
||||
|
||||
# Decorators for easy metric collection
|
||||
def track_time(method_name: str = None, handler_type: str = "unknown"):
|
||||
"""Decorator to track execution time of functions."""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"success"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"error"
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"success"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"error"
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def track_errors(handler_type: str = "unknown", method_name: str = None):
|
||||
"""Decorator to track errors in functions."""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def db_query_time(query_type: str = "unknown", table_name: str = "unknown", operation: str = "unknown"):
|
||||
"""Decorator to track database query execution time."""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"database",
|
||||
func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"database",
|
||||
func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def track_middleware(middleware_name: str):
|
||||
"""Context manager to track middleware execution time."""
|
||||
start_time = time.time()
|
||||
try:
|
||||
yield
|
||||
duration = time.time() - start_time
|
||||
metrics.record_middleware(middleware_name, duration, "success")
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_middleware(middleware_name, duration, "error")
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"middleware",
|
||||
middleware_name
|
||||
)
|
||||
raise
|
||||
201
helper_bot/utils/metrics_exporter.py
Normal file
201
helper_bot/utils/metrics_exporter.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Metrics exporter for Prometheus.
|
||||
Provides HTTP endpoint for metrics collection and background metrics collection.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from typing import Optional, Dict, Any
|
||||
from .metrics import metrics
|
||||
|
||||
|
||||
|
||||
class MetricsExporter:
|
||||
"""HTTP server for exposing Prometheus metrics."""
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = web.Application()
|
||||
self.runner: Optional[web.AppRunner] = None
|
||||
self.site: Optional[web.TCPSite] = None
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Setup routes
|
||||
self.app.router.add_get('/metrics', self.metrics_handler)
|
||||
self.app.router.add_get('/health', self.health_handler)
|
||||
self.app.router.add_get('/', self.root_handler)
|
||||
|
||||
async def start(self):
|
||||
"""Start the metrics server."""
|
||||
try:
|
||||
self.runner = web.AppRunner(self.app)
|
||||
await self.runner.setup()
|
||||
|
||||
self.site = web.TCPSite(self.runner, self.host, self.port)
|
||||
await self.site.start()
|
||||
|
||||
self.logger.info(f"Metrics server started on {self.host}:{self.port}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to start metrics server: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the metrics server."""
|
||||
if self.site:
|
||||
await self.site.stop()
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
self.logger.info("Metrics server stopped")
|
||||
|
||||
async def metrics_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /metrics endpoint for Prometheus."""
|
||||
try:
|
||||
# Log request for debugging
|
||||
self.logger.info(f"Metrics request from {request.remote}: {request.headers.get('User-Agent', 'Unknown')}")
|
||||
|
||||
metrics_data = metrics.get_metrics()
|
||||
self.logger.debug(f"Generated metrics: {len(metrics_data)} bytes")
|
||||
|
||||
return web.Response(
|
||||
body=metrics_data,
|
||||
content_type='text/plain; version=0.0.4'
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating metrics: {e}")
|
||||
return web.Response(
|
||||
text=f"Error generating metrics: {e}",
|
||||
status=500
|
||||
)
|
||||
|
||||
async def health_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /health endpoint for health checks."""
|
||||
return web.json_response({
|
||||
"status": "healthy",
|
||||
"service": "telegram-bot-metrics"
|
||||
})
|
||||
|
||||
async def root_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle root endpoint with basic info."""
|
||||
return web.json_response({
|
||||
"service": "Telegram Bot Metrics Exporter",
|
||||
"endpoints": {
|
||||
"/metrics": "Prometheus metrics",
|
||||
"/health": "Health check",
|
||||
"/": "This info"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class BackgroundMetricsCollector:
|
||||
"""Background service for collecting periodic metrics."""
|
||||
|
||||
def __init__(self, db: Optional[Any] = None, interval: int = 60):
|
||||
self.db = db
|
||||
self.interval = interval
|
||||
self.running = False
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def start(self):
|
||||
"""Start background metrics collection."""
|
||||
self.running = True
|
||||
self.logger.info("Background metrics collector started")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
await self._collect_metrics()
|
||||
await asyncio.sleep(self.interval)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in background metrics collection: {e}")
|
||||
await asyncio.sleep(self.interval)
|
||||
|
||||
async def stop(self):
|
||||
"""Stop background metrics collection."""
|
||||
self.running = False
|
||||
self.logger.info("Background metrics collector stopped")
|
||||
|
||||
async def _collect_metrics(self):
|
||||
"""Collect periodic metrics."""
|
||||
try:
|
||||
# Collect active users count if database is available
|
||||
if self.db:
|
||||
await self._collect_user_metrics()
|
||||
|
||||
# Collect system metrics
|
||||
await self._collect_system_metrics()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error collecting metrics: {e}")
|
||||
|
||||
async def _collect_user_metrics(self):
|
||||
"""Collect user-related metrics from database."""
|
||||
try:
|
||||
if hasattr(self.db, 'fetch_one'):
|
||||
# Try to get active users from database if it has async methods
|
||||
try:
|
||||
active_users_query = """
|
||||
SELECT COUNT(DISTINCT user_id) as active_users
|
||||
FROM our_users
|
||||
WHERE date_added > datetime('now', '-1 day')
|
||||
"""
|
||||
result = await self.db.fetch_one(active_users_query)
|
||||
if result:
|
||||
metrics.set_active_users(result['active_users'], 'daily')
|
||||
else:
|
||||
metrics.set_active_users(0, 'daily')
|
||||
except Exception as db_error:
|
||||
self.logger.warning(f"Database query failed, using placeholder: {db_error}")
|
||||
metrics.set_active_users(0, 'daily')
|
||||
else:
|
||||
# For now, set a placeholder value
|
||||
metrics.set_active_users(0, 'daily')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error collecting user metrics: {e}")
|
||||
metrics.set_active_users(0, 'daily')
|
||||
|
||||
async def _collect_system_metrics(self):
|
||||
"""Collect system-level metrics."""
|
||||
try:
|
||||
# Example: collect memory usage, CPU usage, etc.
|
||||
# This can be extended based on your needs
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error collecting system metrics: {e}")
|
||||
|
||||
|
||||
class MetricsManager:
|
||||
"""Main class for managing metrics collection and export."""
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8000, db: Optional[Any] = None):
|
||||
self.exporter = MetricsExporter(host, port)
|
||||
self.collector = BackgroundMetricsCollector(db)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def start(self):
|
||||
"""Start metrics collection and export."""
|
||||
try:
|
||||
# Start metrics exporter
|
||||
await self.exporter.start()
|
||||
|
||||
# Start background collector
|
||||
asyncio.create_task(self.collector.start())
|
||||
|
||||
self.logger.info("Metrics manager started successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to start metrics manager: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Stop metrics collection and export."""
|
||||
try:
|
||||
await self.collector.stop()
|
||||
await self.exporter.stop()
|
||||
self.logger.info("Metrics manager stopped successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping metrics manager: {e}")
|
||||
raise
|
||||
Reference in New Issue
Block a user